From d0c24c0b8cfa0b4d01edd921e28e13796693d883 Mon Sep 17 00:00:00 2001 From: Dave Clarke Date: Tue, 23 Jun 2026 17:20:52 +0100 Subject: [PATCH 1/4] Add Notion plugin Community plugin integrating Notion via the REST API (OAuth 2.0). Indexes workspace users, data sources and pages, and adds data streams for page properties, page content, comments and database schema, plus an overview dashboard and per-object perspectives. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/Notion/v1/configValidation.json | 11 + plugins/Notion/v1/custom_types.json | 23 ++ .../Notion/v1/dataStreams/currentUser.json | 21 ++ .../Notion/v1/dataStreams/dataSourceRows.json | 31 +++ .../v1/dataStreams/dataSourceSchema.json | 22 ++ .../v1/dataStreams/dataSourcesSearch.json | 29 +++ .../Notion/v1/dataStreams/pageComments.json | 42 ++++ .../Notion/v1/dataStreams/pageContent.json | 27 +++ .../Notion/v1/dataStreams/pageProperties.json | 25 ++ .../Notion/v1/dataStreams/pagesSearch.json | 40 ++++ .../v1/dataStreams/scripts/currentUser.js | 10 + .../v1/dataStreams/scripts/dataSourceRows.js | 89 +++++++ .../dataStreams/scripts/dataSourceSchema.js | 21 ++ .../dataStreams/scripts/dataSourcesSearch.js | 10 + .../v1/dataStreams/scripts/pageComments.js | 11 + .../v1/dataStreams/scripts/pageContent.js | 33 +++ .../v1/dataStreams/scripts/pageProperties.js | 88 +++++++ .../v1/dataStreams/scripts/pagesSearch.js | 37 +++ plugins/Notion/v1/dataStreams/usersList.json | 28 +++ .../Notion/v1/defaultContent/manifest.json | 8 + .../defaultContent/notionDataSource.dash.json | 168 ++++++++++++++ .../v1/defaultContent/notionPage.dash.json | 125 ++++++++++ .../v1/defaultContent/notionUser.dash.json | 48 ++++ .../v1/defaultContent/overview.dash.json | 219 ++++++++++++++++++ plugins/Notion/v1/defaultContent/scopes.json | 38 +++ plugins/Notion/v1/docs/README.md | 52 +++++ plugins/Notion/v1/icon.svg | 6 + .../Notion/v1/indexDefinitions/default.json | 52 +++++ plugins/Notion/v1/metadata.json | 61 +++++ plugins/Notion/v1/ui.json | 45 ++++ 30 files changed, 1420 insertions(+) create mode 100644 plugins/Notion/v1/configValidation.json create mode 100644 plugins/Notion/v1/custom_types.json create mode 100644 plugins/Notion/v1/dataStreams/currentUser.json create mode 100644 plugins/Notion/v1/dataStreams/dataSourceRows.json create mode 100644 plugins/Notion/v1/dataStreams/dataSourceSchema.json create mode 100644 plugins/Notion/v1/dataStreams/dataSourcesSearch.json create mode 100644 plugins/Notion/v1/dataStreams/pageComments.json create mode 100644 plugins/Notion/v1/dataStreams/pageContent.json create mode 100644 plugins/Notion/v1/dataStreams/pageProperties.json create mode 100644 plugins/Notion/v1/dataStreams/pagesSearch.json create mode 100644 plugins/Notion/v1/dataStreams/scripts/currentUser.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/pageComments.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/pageContent.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/pageProperties.js create mode 100644 plugins/Notion/v1/dataStreams/scripts/pagesSearch.js create mode 100644 plugins/Notion/v1/dataStreams/usersList.json create mode 100644 plugins/Notion/v1/defaultContent/manifest.json create mode 100644 plugins/Notion/v1/defaultContent/notionDataSource.dash.json create mode 100644 plugins/Notion/v1/defaultContent/notionPage.dash.json create mode 100644 plugins/Notion/v1/defaultContent/notionUser.dash.json create mode 100644 plugins/Notion/v1/defaultContent/overview.dash.json create mode 100644 plugins/Notion/v1/defaultContent/scopes.json create mode 100644 plugins/Notion/v1/docs/README.md create mode 100644 plugins/Notion/v1/icon.svg create mode 100644 plugins/Notion/v1/indexDefinitions/default.json create mode 100644 plugins/Notion/v1/metadata.json create mode 100644 plugins/Notion/v1/ui.json diff --git a/plugins/Notion/v1/configValidation.json b/plugins/Notion/v1/configValidation.json new file mode 100644 index 00000000..e8ae3382 --- /dev/null +++ b/plugins/Notion/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "currentUser" }, + "required": true, + "error": "Could not connect to Notion. Make sure you have signed in and authorised access to your workspace.", + "success": "Connected to Notion successfully." + } + ] +} diff --git a/plugins/Notion/v1/custom_types.json b/plugins/Notion/v1/custom_types.json new file mode 100644 index 00000000..df15bcb4 --- /dev/null +++ b/plugins/Notion/v1/custom_types.json @@ -0,0 +1,23 @@ +[ + { + "name": "Notion User", + "sourceType": "Notion User", + "icon": "user", + "singular": "User", + "plural": "Users" + }, + { + "name": "Notion Data Source", + "sourceType": "Notion Data Source", + "icon": "table", + "singular": "Data Source", + "plural": "Data Sources" + }, + { + "name": "Notion Page", + "sourceType": "Notion Page", + "icon": "file-lines", + "singular": "Page", + "plural": "Pages" + } +] diff --git a/plugins/Notion/v1/dataStreams/currentUser.json b/plugins/Notion/v1/dataStreams/currentUser.json new file mode 100644 index 00000000..8321d9ba --- /dev/null +++ b/plugins/Notion/v1/dataStreams/currentUser.json @@ -0,0 +1,21 @@ +{ + "name": "currentUser", + "displayName": "Current User", + "description": "The authenticated Notion bot user and its workspace", + "tags": ["Account"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me", + "postRequestScript": "currentUser.js" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "workspaceName", "displayName": "Workspace", "shape": "string" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/dataSourceRows.json b/plugins/Notion/v1/dataStreams/dataSourceRows.json new file mode 100644 index 00000000..584b562c --- /dev/null +++ b/plugins/Notion/v1/dataStreams/dataSourceRows.json @@ -0,0 +1,31 @@ +{ + "name": "dataSourceRows", + "displayName": "Data Source Contents", + "description": "Pages within a data source with their property values", + "tags": ["Data Sources", "Pages"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "post", + "endpointPath": "data_sources/{{object.rawId}}/query", + "postBody": {}, + "postRequestScript": "dataSourceRows.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "body", "path": "page_size", "value": 100 }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "body", "path": "start_cursor" } + } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Data Source"] } + }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "createdTime", "displayName": "Created", "shape": "date", "visible": false }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }], "visible": false }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/dataSourceSchema.json b/plugins/Notion/v1/dataStreams/dataSourceSchema.json new file mode 100644 index 00000000..0a82a1fd --- /dev/null +++ b/plugins/Notion/v1/dataStreams/dataSourceSchema.json @@ -0,0 +1,22 @@ +{ + "name": "dataSourceSchema", + "displayName": "Data Source Schema", + "description": "Property names and types (schema) for a single data source", + "tags": ["Data Sources"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "data_sources/{{object.rawId}}", + "paging": { "mode": "none" }, + "postRequestScript": "dataSourceSchema.js" + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Data Source"] } + }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "details", "displayName": "Details", "shape": "string" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/dataSourcesSearch.json b/plugins/Notion/v1/dataStreams/dataSourcesSearch.json new file mode 100644 index 00000000..3cace383 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/dataSourcesSearch.json @@ -0,0 +1,29 @@ +{ + "name": "dataSourcesSearch", + "displayName": "Data Sources", + "description": "Data sources (database tables) shared with the integration", + "tags": ["Data Sources"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "search", + "postBody": { "filter": { "property": "object", "value": "data_source" } }, + "postRequestScript": "dataSourcesSearch.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "body", "path": "page_size", "value": 100 }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "body", "path": "start_cursor" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "createdTime", "displayName": "Created", "shape": "date" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }] }, + { "name": "parentDatabaseId", "displayName": "Parent Database ID", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pageComments.json b/plugins/Notion/v1/dataStreams/pageComments.json new file mode 100644 index 00000000..45af2433 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pageComments.json @@ -0,0 +1,42 @@ +{ + "name": "pageComments", + "displayName": "Page Comments", + "description": "Comments and discussion threads on a single Notion page", + "tags": ["Comments"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "comments", + "getArgs": [ + { "key": "block_id", "value": "{{object.rawId}}" } + ], + "postRequestScript": "pageComments.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "queryArg", "path": "start_cursor" } + }, + "errorHandling": { + "type": "script", + "script": "result = response.status === 403 ? 'Notion did not grant the Read comments capability. Enable \"Read comments\" on your Notion integration, then reconnect the plugin in SquaredUp.' : ((data && data.message) || ('Notion comments API returned status ' + response.status));" + } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page"] } + }, + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "createdTime", "displayName": "Created", "shape": "date" }, + { "name": "text", "displayName": "Comment", "shape": "string", "role": "label" }, + { "name": "createdById", "displayName": "Created By ID", "shape": "string", "visible": false }, + { + "name": "author", + "displayName": "Author", + "sourceId": "createdById", + "sourceType": "Notion User", + "objectPropertyPath": "name" + } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pageContent.json b/plugins/Notion/v1/dataStreams/pageContent.json new file mode 100644 index 00000000..1634200e --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pageContent.json @@ -0,0 +1,27 @@ +{ + "name": "pageContent", + "displayName": "Page Content", + "description": "Block-level content of a Notion page — paragraphs, headings, to-do items, lists, quotes, callouts and code blocks", + "tags": ["Pages"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "blocks/{{object.rawId}}/children", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "queryArg", "path": "start_cursor" } + }, + "postRequestScript": "pageContent.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Notion Page"] } }, + "metadata": [ + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "text", "displayName": "Text", "shape": "string", "role": "label" }, + { "name": "checked", "displayName": "Checked", "shape": "boolean" }, + { "name": "hasChildren", "displayName": "Has Children", "shape": "boolean", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pageProperties.json b/plugins/Notion/v1/dataStreams/pageProperties.json new file mode 100644 index 00000000..fc5e2364 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pageProperties.json @@ -0,0 +1,25 @@ +{ + "name": "pageProperties", + "displayName": "Page Properties", + "description": "Property values for a single Notion page", + "tags": ["Pages"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "pages/{{object.rawId}}", + "postRequestScript": "pageProperties.js", + "paging": { "mode": "none" } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page"] } + }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "createdTime", "displayName": "Created", "shape": "date", "visible": false }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }], "visible": false }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pagesSearch.json b/plugins/Notion/v1/dataStreams/pagesSearch.json new file mode 100644 index 00000000..090449e7 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pagesSearch.json @@ -0,0 +1,40 @@ +{ + "name": "pagesSearch", + "displayName": "Pages", + "description": "Pages shared with the integration, including database rows", + "tags": ["Pages"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "search", + "postBody": { "filter": { "property": "object", "value": "page" } }, + "postRequestScript": "pagesSearch.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "body", "path": "page_size", "value": 100 }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "body", "path": "start_cursor" } + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "scope", + "label": "Page or data source (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page", "Notion Data Source"] } + } + } + ], + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "createdTime", "displayName": "Created", "shape": "date" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }] }, + { "name": "parentType", "displayName": "Parent Type", "shape": "string", "visible": false }, + { "name": "parentDataSourceId", "displayName": "Parent Data Source ID", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/scripts/currentUser.js b/plugins/Notion/v1/dataStreams/scripts/currentUser.js new file mode 100644 index 00000000..6417c8be --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/currentUser.js @@ -0,0 +1,10 @@ +// /v1/users/me returns a single user object (the integration's bot user), +// not an array — wrap it as one row and surface the workspace name. +result = [ + { + id: data.id, + name: data.name || (data.bot && data.bot.workspace_name) || "Notion bot", + type: data.type, + workspaceName: data.bot && data.bot.workspace_name + } +]; diff --git a/plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js b/plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js new file mode 100644 index 00000000..03016c2e --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js @@ -0,0 +1,89 @@ +// POST /v1/data_sources/{id}/query returns the pages (rows) of a data source, +// accumulated across pages under data.results. Each page's `properties` is a bag +// of typed Notion property values keyed by the property name; flatten each to a +// scalar so the custom fields become plain columns. +const plain = (rich) => (rich || []).map((t) => t.plain_text).join(""); + +const valueOf = (prop) => { + if (!prop) return null; + switch (prop.type) { + case "title": + return plain(prop.title); + case "rich_text": + return plain(prop.rich_text); + case "number": + return prop.number; + case "select": + return prop.select ? prop.select.name : null; + case "status": + return prop.status ? prop.status.name : null; + case "multi_select": + return (prop.multi_select || []).map((s) => s.name).join(", "); + case "date": + return prop.date ? prop.date.start : null; + case "checkbox": + return prop.checkbox; + case "url": + return prop.url; + case "email": + return prop.email; + case "phone_number": + return prop.phone_number; + case "people": + return (prop.people || []).map((p) => p.name || p.id).join(", "); + case "files": + return (prop.files || []).map((f) => f.name).join(", "); + case "created_time": + return prop.created_time; + case "last_edited_time": + return prop.last_edited_time; + case "created_by": + return prop.created_by ? prop.created_by.name || prop.created_by.id : null; + case "last_edited_by": + return prop.last_edited_by ? prop.last_edited_by.name || prop.last_edited_by.id : null; + case "unique_id": + return prop.unique_id + ? (prop.unique_id.prefix ? prop.unique_id.prefix + "-" : "") + prop.unique_id.number + : null; + case "formula": + return prop.formula + ? prop.formula.string ?? + prop.formula.number ?? + prop.formula.boolean ?? + (prop.formula.date ? prop.formula.date.start : null) + : null; + case "rollup": + if (!prop.rollup) return null; + if (prop.rollup.type === "array") { + return (prop.rollup.array || []).map((item) => valueOf(item)).filter((v) => v != null).join(", "); + } + return prop.rollup.number ?? (prop.rollup.date ? prop.rollup.date.start : null); + case "relation": + return (prop.relation || []).map((r) => r.id).join(", "); + default: + return null; + } +}; + +result = (data.results || []).map((page) => { + const props = page.properties || {}; + const row = { + id: page.id, + url: page.url || null, + createdTime: page.created_time || null, + lastEditedTime: page.last_edited_time || null + }; + let title = ""; + for (const key of Object.keys(props)) { + const prop = props[key]; + const value = valueOf(prop); + // The title property is surfaced as `name` — don't also emit it as a duplicate column. + if (prop && prop.type === "title") { + title = value || title; + continue; + } + row[key] = value; + } + row.name = title || "Untitled"; + return row; +}); diff --git a/plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js b/plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js new file mode 100644 index 00000000..eb4d4351 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js @@ -0,0 +1,21 @@ +// dataStreams/scripts/dataSourceSchema.js +// data is the single data source object: { id, title, properties: { : { id, name, type, ... } } } +// Iterate properties (object keyed by property name) → one row per property. + +result = Object.values(data.properties || {}).map(function (prop) { + var details = ""; + var typeConfig = prop[prop.type]; + + if (prop.type === "select" || prop.type === "multi_select") { + var options = typeConfig && typeConfig.options ? typeConfig.options : []; + details = options.map(function (o) { return o.name; }).join(", "); + } else if (prop.type === "relation") { + details = (typeConfig && typeConfig.database_id) ? typeConfig.database_id : ""; + } + + return { + name: prop.name, + type: prop.type, + details: details + }; +}); diff --git a/plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js b/plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js new file mode 100644 index 00000000..5ca9a87e --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js @@ -0,0 +1,10 @@ +// POST /v1/search (filter data_source) returns paged results accumulated under +// data.results. The title is a rich-text array; flatten it to plain text. +result = (data.results || []).map((ds) => ({ + id: ds.id, + name: (ds.title || []).map((t) => t.plain_text).join("").trim() || "Untitled", + createdTime: ds.created_time || null, + lastEditedTime: ds.last_edited_time || null, + url: ds.url || null, + parentDatabaseId: (ds.parent && ds.parent.database_id) || null +})); diff --git a/plugins/Notion/v1/dataStreams/scripts/pageComments.js b/plugins/Notion/v1/dataStreams/scripts/pageComments.js new file mode 100644 index 00000000..ad621494 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pageComments.js @@ -0,0 +1,11 @@ +// GET /v1/comments?block_id= returns paged results under data.results. +// Each comment has: id, created_time, created_by: { object: "user", id }, +// rich_text: [{ plain_text, ... }], parent. +// Join all plain_text segments to produce a single text string per comment. + +result = (data.results || []).map((comment) => ({ + id: comment.id, + createdTime: comment.created_time || null, + createdById: (comment.created_by && comment.created_by.id) || null, + text: (comment.rich_text || []).map((rt) => rt.plain_text || "").join("").trim() || null +})); diff --git a/plugins/Notion/v1/dataStreams/scripts/pageContent.js b/plugins/Notion/v1/dataStreams/scripts/pageContent.js new file mode 100644 index 00000000..6abcdb85 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pageContent.js @@ -0,0 +1,33 @@ +// blocks/{{pageId}}/children returns top-level blocks only (nested children require +// separate calls per block — not fetched here). Each block has a `type` key and a +// same-named property containing a `rich_text` array (for text-bearing types) and +// optionally a `checked` boolean (to_do blocks only). +const TEXT_TYPES = new Set([ + "paragraph", + "heading_1", + "heading_2", + "heading_3", + "bulleted_list_item", + "numbered_list_item", + "to_do", + "toggle", + "quote", + "callout", + "code" +]); + +const plain = (richText) => + (richText || []).map((t) => t.plain_text || "").join(""); + +result = (data.results || []).map((block) => { + const type = block.type; + const blockData = block[type] || {}; + const text = TEXT_TYPES.has(type) ? plain(blockData.rich_text) : ""; + const checked = type === "to_do" ? (blockData.checked === true) : null; + return { + type, + text, + checked, + hasChildren: block.has_children === true + }; +}); diff --git a/plugins/Notion/v1/dataStreams/scripts/pageProperties.js b/plugins/Notion/v1/dataStreams/scripts/pageProperties.js new file mode 100644 index 00000000..bfff238b --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pageProperties.js @@ -0,0 +1,88 @@ +// GET /v1/pages/{id} returns a single page object. Flatten its properties bag +// into a plain row so custom fields become columns. Copied helpers from dataSourceRows.js. +const plain = (rich) => (rich || []).map((t) => t.plain_text).join(""); + +const valueOf = (prop) => { + if (!prop) return null; + switch (prop.type) { + case "title": + return plain(prop.title); + case "rich_text": + return plain(prop.rich_text); + case "number": + return prop.number; + case "select": + return prop.select ? prop.select.name : null; + case "status": + return prop.status ? prop.status.name : null; + case "multi_select": + return (prop.multi_select || []).map((s) => s.name).join(", "); + case "date": + return prop.date ? prop.date.start : null; + case "checkbox": + return prop.checkbox; + case "url": + return prop.url; + case "email": + return prop.email; + case "phone_number": + return prop.phone_number; + case "people": + return (prop.people || []).map((p) => p.name || p.id).join(", "); + case "files": + return (prop.files || []).map((f) => f.name).join(", "); + case "created_time": + return prop.created_time; + case "last_edited_time": + return prop.last_edited_time; + case "created_by": + return prop.created_by ? prop.created_by.name || prop.created_by.id : null; + case "last_edited_by": + return prop.last_edited_by ? prop.last_edited_by.name || prop.last_edited_by.id : null; + case "unique_id": + return prop.unique_id + ? (prop.unique_id.prefix ? prop.unique_id.prefix + "-" : "") + prop.unique_id.number + : null; + case "formula": + return prop.formula + ? prop.formula.string ?? + prop.formula.number ?? + prop.formula.boolean ?? + (prop.formula.date ? prop.formula.date.start : null) + : null; + case "rollup": + if (!prop.rollup) return null; + if (prop.rollup.type === "array") { + return (prop.rollup.array || []).map((item) => valueOf(item)).filter((v) => v != null).join(", "); + } + return prop.rollup.number ?? (prop.rollup.date ? prop.rollup.date.start : null); + case "relation": + return (prop.relation || []).map((r) => r.id).join(", "); + default: + return null; + } +}; + +// data is the single page object returned by GET /v1/pages/{id} +const page = data; +const props = page.properties || {}; +const row = { + id: page.id, + url: page.url || null, + createdTime: page.created_time || null, + lastEditedTime: page.last_edited_time || null +}; +let title = ""; +for (const key of Object.keys(props)) { + const prop = props[key]; + const value = valueOf(prop); + // The title property is surfaced as `name` — don't also emit it as a duplicate column. + if (prop && prop.type === "title") { + title = value || title; + continue; + } + row[key] = value; +} +row.name = title || "Untitled"; + +result = [row]; diff --git a/plugins/Notion/v1/dataStreams/scripts/pagesSearch.js b/plugins/Notion/v1/dataStreams/scripts/pagesSearch.js new file mode 100644 index 00000000..0ce369f9 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pagesSearch.js @@ -0,0 +1,37 @@ +// POST /v1/search (filter page) returns paged results accumulated under +// data.results. A page's title lives in a dynamically-named property whose +// type is "title" — scan the properties bag to find it. +const titleOf = (page) => { + const props = page.properties || {}; + for (const key of Object.keys(props)) { + const p = props[key]; + if (p && p.type === "title" && Array.isArray(p.title)) { + return p.title.map((t) => t.plain_text).join("").trim(); + } + } + return ""; +}; + +const rows = (data.results || []).map((page) => { + const parent = page.parent || {}; + return { + id: page.id, + name: titleOf(page) || "Untitled", + createdTime: page.created_time || null, + lastEditedTime: page.last_edited_time || null, + url: page.url || null, + parentType: parent.type || null, + parentDataSourceId: parent.data_source_id || null + }; +}); + +// Optional scope filter: page(s) or data source(s) selected via the ui "scope" picker. +// context.config.scope is an array of object envelopes; each rawId may itself be a +// single-element array — unwrap it before comparing. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.scope) || []; +const selectedIds = new Set(selected.map((o) => unwrap(o.rawId)).filter(Boolean)); + +result = selectedIds.size + ? rows.filter((r) => selectedIds.has(r.id) || selectedIds.has(r.parentDataSourceId)) + : rows; diff --git a/plugins/Notion/v1/dataStreams/usersList.json b/plugins/Notion/v1/dataStreams/usersList.json new file mode 100644 index 00000000..f22c3594 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/usersList.json @@ -0,0 +1,28 @@ +{ + "name": "usersList", + "displayName": "Users", + "description": "Members of the Notion workspace", + "tags": ["Users"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users", + "pathToData": "results", + "expandInnerObjects": true, + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "queryArg", "path": "start_cursor" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "person.email", "displayName": "Email", "shape": "string" }, + { "name": "avatar_url", "displayName": "Avatar URL", "shape": "url", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/defaultContent/manifest.json b/plugins/Notion/v1/defaultContent/manifest.json new file mode 100644 index 00000000..00d2358c --- /dev/null +++ b/plugins/Notion/v1/defaultContent/manifest.json @@ -0,0 +1,8 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "notionUser", "type": "dashboard" }, + { "name": "notionDataSource", "type": "dashboard" }, + { "name": "notionPage", "type": "dashboard" } + ] +} diff --git a/plugins/Notion/v1/defaultContent/notionDataSource.dash.json b/plugins/Notion/v1/defaultContent/notionDataSource.dash.json new file mode 100644 index 00000000..c73b36ff --- /dev/null +++ b/plugins/Notion/v1/defaultContent/notionDataSource.dash.json @@ -0,0 +1,168 @@ +{ + "name": "Data Source", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Notion Data Source]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "18b6c622-ae69-468e-8013-7083e2a8831d", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Details", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Notion Data Sources]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Data Source]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "45fa56ff-b832-4e9c-9fb4-f71383ee3910", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pages in this Data Source", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pagesSearch]}}", + "name": "pagesSearch", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "scope": { + "variable": "{{variables.[Notion Data Source]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Notion Data Sources]}}" + } + }, + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "lastEditedTime", "url"], + "hiddenColumns": ["id", "createdTime", "parentType", "parentDataSourceId"] + } + } + } + } + }, + { + "i": "7c1e9a04-2d5b-4f6a-9c3e-1b2a3c4d5e6f", + "x": 0, + "y": 8, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Contents", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourceRows]}}", + "name": "dataSourceRows", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "scope": { + "scope": "{{scopes.[Notion Data Sources]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Data Source]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "hiddenColumns": ["id", "createdTime"] + } + } + } + } + }, + { + "i": "9d2f4b81-6a3c-4e72-bf90-5c1d2e3f4a5b", + "x": 0, + "y": 13, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Fields", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourceSchema]}}", + "name": "dataSourceSchema", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Data Sources]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Data Source]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "type", "details"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/notionPage.dash.json b/plugins/Notion/v1/defaultContent/notionPage.dash.json new file mode 100644 index 00000000..ad8b5c70 --- /dev/null +++ b/plugins/Notion/v1/defaultContent/notionPage.dash.json @@ -0,0 +1,125 @@ +{ + "name": "Page", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Notion Page]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "60514e48-d346-4df3-b38e-9a620f254d25", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Page]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pageProperties]}}", + "name": "pageProperties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Pages]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Page]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "a4d2f1b6-3c8e-4a90-bb71-2f5c6d7e8a91", + "x": 0, + "y": 4, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Content", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Page]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pageContent]}}", + "name": "pageContent", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Pages]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Page]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["text", "type", "checked"], + "hiddenColumns": ["hasChildren"] + } + } + } + } + }, + { + "i": "c5e3a2d7-4f9b-4c01-9d82-3a6b7c8d9e02", + "x": 0, + "y": 9, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Comments", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Page]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pageComments]}}", + "name": "pageComments", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Pages]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Page]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["createdTime", "author", "text"], + "hiddenColumns": ["id", "createdById"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/notionUser.dash.json b/plugins/Notion/v1/defaultContent/notionUser.dash.json new file mode 100644 index 00000000..bc84b73c --- /dev/null +++ b/plugins/Notion/v1/defaultContent/notionUser.dash.json @@ -0,0 +1,48 @@ +{ + "name": "User", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Notion User]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "56eb70db-4900-4fcc-91d1-1a6bf0f21ddc", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Details", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion User]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Notion Users]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion User]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/overview.dash.json b/plugins/Notion/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..6ab930e2 --- /dev/null +++ b/plugins/Notion/v1/defaultContent/overview.dash.json @@ -0,0 +1,219 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "6e8b4dc7-b902-4bb8-8f70-1f149eac93bc", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Users", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[usersList]}}", + "name": "usersList", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "679c5421-4490-4a8b-b501-05acdd1fed39", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Data Sources", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourcesSearch]}}", + "name": "dataSourcesSearch", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "0d924bf2-0b5a-46df-bb3e-d1a5b87e2d18", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pages", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pagesSearch]}}", + "name": "pagesSearch", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "f26a5ac7-b229-4cb1-827e-55b753c11125", + "x": 0, + "y": 2, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recently Edited Pages", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pagesSearch]}}", + "name": "pagesSearch", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "lastEditedTime", "url"], + "hiddenColumns": ["id", "createdTime", "parentType", "parentDataSourceId"] + } + } + } + } + }, + { + "i": "c5b2aadc-a62f-4894-8d82-1f90ab71689e", + "x": 0, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Users", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[usersList]}}", + "name": "usersList", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "type", "person.email"], + "hiddenColumns": ["id", "avatar_url"] + } + } + } + } + }, + { + "i": "5e6b33f6-e0fd-4420-9778-e5b11110100d", + "x": 2, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Data Sources", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourcesSearch]}}", + "name": "dataSourcesSearch", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "lastEditedTime", "url"], + "hiddenColumns": ["id", "createdTime", "parentDatabaseId"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/scopes.json b/plugins/Notion/v1/defaultContent/scopes.json new file mode 100644 index 00000000..eddf9a55 --- /dev/null +++ b/plugins/Notion/v1/defaultContent/scopes.json @@ -0,0 +1,38 @@ +[ + { + "name": "Notion Users", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion User"] } + }, + "variable": { + "name": "Notion User", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Notion Data Sources", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Data Source"] } + }, + "variable": { + "name": "Notion Data Source", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Notion Pages", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page"] } + }, + "variable": { + "name": "Notion Page", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Notion/v1/docs/README.md b/plugins/Notion/v1/docs/README.md new file mode 100644 index 00000000..8bba8a97 --- /dev/null +++ b/plugins/Notion/v1/docs/README.md @@ -0,0 +1,52 @@ +# Notion + +Bring your Notion workspace into SquaredUp. This plugin indexes your workspace **users**, **data sources** (the tables/databases shared with the integration) and **pages**, and lets you explore page properties, page content, comments and database schemas — with dashboards that summarise your workspace, surface recently-edited content, and let you drill into any data source or page. + +## What this plugin monitors + +- **Users** — the people and bots that are members of your Notion workspace. +- **Data sources** — the individual tables of data that live under your Notion databases, for every database shared with the integration. +- **Pages** — the pages shared with the integration, including pages that are rows inside a data source. Each page records its title, URL, parent, and created / last-edited times. + +Out-of-the-box dashboards give you a workspace **Overview** plus a **perspective** for each user, data source and page. + +## Before you start — create a Notion integration + +This plugin connects using **OAuth 2.0**, so you need a Notion **public integration** to obtain a Client ID and Client Secret. + +1. Go to [Notion → My integrations](https://www.notion.so/my-integrations) and click **New integration**. +2. Give it a name (e.g. *SquaredUp*) and associate it with the workspace you want to monitor. +3. On the integration's **Configuration** tab, set the integration type to **Public**. Fill in the required company/organisation details (name, website, etc.) — Notion requires these for a public integration. +4. Under **Redirect URIs**, add the SquaredUp redirect URI for your region — copy the one that matches your tenant: + - **US:** `https://app.squaredup.com/settings/pluginsoauth2` + - **EU:** `https://eu.app.squaredup.com/settings/pluginsoauth2` +5. Under **Capabilities**, leave the default read capabilities enabled (the plugin only reads content). If you want to use the **Page Comments** data stream, also enable the **Read comments** capability — it is not on by default, and without it comment requests return a 403. +6. From the **Configuration** tab, copy the **OAuth client ID** and **OAuth client secret** — you'll paste these into SquaredUp. +7. **Share content with the integration.** Notion only returns content that has been explicitly shared with the integration. In each page or database you want monitored, open the **•••** menu → **Connections** → **Connect to** → select your integration. Sharing a parent page shares its children too. + +When you add the plugin in SquaredUp, paste the Client ID and Client Secret, then click **Sign in with Notion** and approve access to the pages/databases you selected. + +## Configuration fields + +| Field | Description | Required | +| --- | --- | --- | +| **Client ID** | The *OAuth client ID* from your Notion integration's Configuration tab. | Yes | +| **Client Secret** | The *OAuth client secret* from your Notion integration's Configuration tab. Stored securely. | Yes | +| **Sign in with Notion** | Launches the Notion OAuth consent screen. After approving, the connection is authenticated. | Yes | + +## What gets indexed + +| Object type | Description | +| --- | --- | +| **Notion User** | A member of the workspace — a person (with email where available) or a bot. | +| **Notion Data Source** | A table of data under a Notion database that has been shared with the integration. | +| **Notion Page** | A page shared with the integration, including database rows. Linked in the graph to its parent data source. | + +## Known limitations + +- **Only shared content is visible.** The Notion API only returns pages, databases and data sources that have been explicitly shared with the integration. Anything not shared will not appear in SquaredUp. Listing workspace **users** additionally requires an OAuth/integration token (personal access tokens cannot list users). +- **No historical data.** The Notion API does not expose a queryable time range, so all data is current-state. Tiles can still highlight stale or recently-edited content using each object's last-edited timestamp, but there are no trend/time-series charts. +- **Rate limits.** Notion limits requests to roughly **3 requests per second** per integration. Large workspaces import more slowly; bursts may be throttled (HTTP 429). +- **Page volume.** Every row in a database is itself a page, so workspaces with large databases can import a very large number of `Notion Page` objects. +- **Comments require an extra capability.** The Page Comments data stream needs the integration's **Read comments** capability (see setup step 5). Without it, the stream returns no data. +- **Page content is top-level only.** The Page Content data stream returns a page's top-level blocks; nested/child blocks (content inside toggles, columns, etc.) are not expanded. diff --git a/plugins/Notion/v1/icon.svg b/plugins/Notion/v1/icon.svg new file mode 100644 index 00000000..9fc27e22 --- /dev/null +++ b/plugins/Notion/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/Notion/v1/indexDefinitions/default.json b/plugins/Notion/v1/indexDefinitions/default.json new file mode 100644 index 00000000..69bc647e --- /dev/null +++ b/plugins/Notion/v1/indexDefinitions/default.json @@ -0,0 +1,52 @@ +{ + "steps": [ + { + "name": "users", + "dataStream": { "name": "usersList" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Notion User" }, + "properties": [ + { "userType": "type" }, + { "email": "person.email" }, + { "avatarUrl": "avatar_url" } + ] + } + }, + { + "name": "dataSources", + "dataStream": { "name": "dataSourcesSearch" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Notion Data Source" }, + "properties": [ + "createdTime", + "lastEditedTime", + "url", + "parentDatabaseId" + ] + } + }, + { + "name": "pages", + "dataStream": { "name": "pagesSearch" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Notion Page" }, + "properties": [ + "createdTime", + "lastEditedTime", + "url", + "parentType", + "parentDataSourceId" + ] + } + } + ] +} diff --git a/plugins/Notion/v1/metadata.json b/plugins/Notion/v1/metadata.json new file mode 100644 index 00000000..7691255f --- /dev/null +++ b/plugins/Notion/v1/metadata.json @@ -0,0 +1,61 @@ +{ + "name": "notion", + "displayName": "Notion", + "version": "1.2.4", + "author": { + "name": "@clarkd", + "type": "community" + }, + "description": "Bring your Notion workspace into SquaredUp — index users, databases and pages, and surface page properties, content, comments and database schemas.", + "category": "Collaboration", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "notion", + "workspace", + "pages", + "databases", + "knowledge base", + "collaboration" + ], + "objectTypes": [ + "Notion User", + "Notion Data Source", + "Notion Page" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Notion/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Notion/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.notion.com/v1", + "authMode": "oauth2", + "oauth2GrantType": "authCode", + "oauth2AuthUrl": "https://api.notion.com/v1/oauth/authorize", + "oauth2TokenUrl": "https://api.notion.com/v1/oauth/token", + "oauth2ClientId": "{{clientId}}", + "oauth2ClientSecret": "{{clientSecret}}", + "oauth2ClientSecretLocationDuringAuth": "header", + "oauth2AuthExtraArgs": [ + { "key": "owner", "value": "user" } + ], + "headers": [ + { "key": "Notion-Version", "value": "2026-03-11" } + ], + "queryArgs": [] + } + } +} diff --git a/plugins/Notion/v1/ui.json b/plugins/Notion/v1/ui.json new file mode 100644 index 00000000..1dfc0961 --- /dev/null +++ b/plugins/Notion/v1/ui.json @@ -0,0 +1,45 @@ +[ + { + "name": "clientId", + "label": "Client ID", + "type": "text", + "help": "Your Notion integration's **OAuth client ID**, found on the integration's Configuration tab. [Manage integrations](https://www.notion.so/my-integrations)", + "validation": { + "required": true + } + }, + { + "name": "clientSecret", + "label": "Client Secret", + "type": "password", + "help": "Your Notion integration's **OAuth client secret**, found on the integration's Configuration tab. [Manage integrations](https://www.notion.so/my-integrations)", + "validation": { + "required": true + } + }, + { + "type": "fieldGroup", + "name": "signInGroup", + "label": "Sign in", + "visible": { + "clientId": { + "type": "regex", + "pattern": "(.+)" + }, + "clientSecret": { + "type": "regex", + "pattern": "(.+)" + } + }, + "fields": [ + { + "type": "oAuth2", + "name": "oauth2AuthCodeSignIn", + "label": "Sign in with Notion", + "validation": { + "required": true + } + } + ] + } +] From 56645419d2ed108cdfbfc1ca2c90b6397d050313 Mon Sep 17 00:00:00 2001 From: Dave Clarke Date: Wed, 24 Jun 2026 14:24:10 +0100 Subject: [PATCH 2/4] Update ui.json --- plugins/Notion/v1/ui.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Notion/v1/ui.json b/plugins/Notion/v1/ui.json index 1dfc0961..ce3350e4 100644 --- a/plugins/Notion/v1/ui.json +++ b/plugins/Notion/v1/ui.json @@ -4,6 +4,7 @@ "label": "Client ID", "type": "text", "help": "Your Notion integration's **OAuth client ID**, found on the integration's Configuration tab. [Manage integrations](https://www.notion.so/my-integrations)", + "placeholder": "e.g. 000000000-0000-0000-0000-000000000000", "validation": { "required": true } @@ -13,6 +14,7 @@ "label": "Client Secret", "type": "password", "help": "Your Notion integration's **OAuth client secret**, found on the integration's Configuration tab. [Manage integrations](https://www.notion.so/my-integrations)", + "placeholder": "e.g. secret_00000000000000000000000000000000", "validation": { "required": true } From 94c60ee22714c74601cbb850a150ea8a2412c444 Mon Sep 17 00:00:00 2001 From: Dave Clarke Date: Thu, 25 Jun 2026 16:04:51 +0100 Subject: [PATCH 3/4] Update README.md --- plugins/Notion/v1/docs/README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/Notion/v1/docs/README.md b/plugins/Notion/v1/docs/README.md index 8bba8a97..70b4c0dc 100644 --- a/plugins/Notion/v1/docs/README.md +++ b/plugins/Notion/v1/docs/README.md @@ -14,18 +14,23 @@ Out-of-the-box dashboards give you a workspace **Overview** plus a **perspective This plugin connects using **OAuth 2.0**, so you need a Notion **public integration** to obtain a Client ID and Client Secret. -1. Go to [Notion → My integrations](https://www.notion.so/my-integrations) and click **New integration**. +1. Go to [Notion → My integrations](https://www.notion.so/my-integrations) and click **New connection**. 2. Give it a name (e.g. *SquaredUp*) and associate it with the workspace you want to monitor. -3. On the integration's **Configuration** tab, set the integration type to **Public**. Fill in the required company/organisation details (name, website, etc.) — Notion requires these for a public integration. -4. Under **Redirect URIs**, add the SquaredUp redirect URI for your region — copy the one that matches your tenant: +3. Select OAuth as the Authentication method +4. If required, select a specific workspace. +5. Under **Redirect URIs**, add the SquaredUp redirect URI for your region — copy the one that matches your tenant: - **US:** `https://app.squaredup.com/settings/pluginsoauth2` - **EU:** `https://eu.app.squaredup.com/settings/pluginsoauth2` -5. Under **Capabilities**, leave the default read capabilities enabled (the plugin only reads content). If you want to use the **Page Comments** data stream, also enable the **Read comments** capability — it is not on by default, and without it comment requests return a 403. -6. From the **Configuration** tab, copy the **OAuth client ID** and **OAuth client secret** — you'll paste these into SquaredUp. -7. **Share content with the integration.** Notion only returns content that has been explicitly shared with the integration. In each page or database you want monitored, open the **•••** menu → **Connections** → **Connect to** → select your integration. Sharing a parent page shares its children too. +6. Click Create connection +7. Under **Capabilities**, uncheck 'Update content' and 'Insert content' - only read permissions are required. If you want to use the **Page Comments** data stream, also enable the **Read comments** capability. +8. Click Save connection +6. Under 'OAuth connection', copy the **Client ID** and **Client secret** — you'll paste these into SquaredUp. When you add the plugin in SquaredUp, paste the Client ID and Client Secret, then click **Sign in with Notion** and approve access to the pages/databases you selected. +### Share content with the integration +Notion only returns content that has been explicitly shared with the integration. In each page or database you want monitored, open the **•••** menu → **Connections** → **Connect to** → select your integration. Sharing a parent page shares its children too. + ## Configuration fields | Field | Description | Required | From 77b7a0e6120193ec03b32e4f49684ad7c032c5c7 Mon Sep 17 00:00:00 2001 From: Dave Clarke Date: Fri, 26 Jun 2026 09:49:52 +0100 Subject: [PATCH 4/4] Update CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9690e211..b7c19787 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,6 +24,7 @@ plugins/TransportForLondon/* @clarkd plugins/UniFi/* @adamkinniburgh plugins/UptimeRobot/* @kieranlangton plugins/WorldCup2026/* @TimWheeler-SQUP +plugins/Notion/* @clarkd # Fallback – if a plugin has no specified author