From a459819000aed7ad0929d55ac24c0299e02abccf Mon Sep 17 00:00:00 2001 From: Andrzej Chmielewski Date: Tue, 7 Apr 2026 21:52:45 +0200 Subject: [PATCH] chore: upgrade @notionhq/client to 5.17.0 and fix three issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrades the Notion SDK from 5.16.0 to 5.17.0 (also resolves 1 npm vulnerability). Fixes three issues discovered during E2E testing: 1. search/ls --json empty results: Output 'No results found' text instead of [] when --json was active. Now respects output mode. 2. archive command: Only tried pages.update, failing for databases. Now cascades through pages → dataSources → databases, supporting all entity types. Also migrates from deprecated 'archived' field to 'in_trash'. 3. create-page --parent : fetchDatabaseSchema() only tried dataSources.retrieve(), failing when given a database page ID. Now uses resolveDataSourceId() which falls back through dataSources → databases to find the correct data source ID. --- package-lock.json | 8 ++--- package.json | 2 +- src/commands/archive.ts | 50 +++++++++++++++++++++++++++----- src/commands/ls.ts | 8 +++-- src/commands/search.ts | 8 +++-- src/services/database.service.ts | 15 ++++++---- tests/commands/archive.test.ts | 4 +-- 7 files changed, 70 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09723a4..8a3dbaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.3.0", - "@notionhq/client": "^5.16.0", + "@notionhq/client": "^5.17.0", "chalk": "^5.6.0", "commander": "^14.0.0", "yaml": "^2.8.0" @@ -1014,9 +1014,9 @@ } }, "node_modules/@notionhq/client": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-5.16.0.tgz", - "integrity": "sha512-0mxHJNQBLbcG2Y4+oM+XNySr/3zKtK7FKjWhrUo8TB3WCdwtUMhvuooNmEjOo8NLL9qmKm7cCR4GtEsuTD+SNg==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-5.17.0.tgz", + "integrity": "sha512-hSNm3VUW5+Qs9vPOmegS6r1RT8O1jtTE/22wmSmiJc9kkb2YyddWr8SBQruvp06rALn2r2RAh9mLPirUUonsvw==", "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 1f59c58..e0fab09 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@inquirer/prompts": "^8.3.0", - "@notionhq/client": "^5.16.0", + "@notionhq/client": "^5.17.0", "chalk": "^5.6.0", "commander": "^14.0.0", "yaml": "^2.8.0" diff --git a/src/commands/archive.ts b/src/commands/archive.ts index d5714d0..7d322b6 100644 --- a/src/commands/archive.ts +++ b/src/commands/archive.ts @@ -6,12 +6,48 @@ import { parseNotionId, toUuid } from '../notion/url-parser.js'; import { formatJSON, getOutputMode } from '../output/format.js'; import { reportTokenSource } from '../output/stderr.js'; +/** + * Try to archive as a page first. If that fails (e.g. the ID is a database + * or data source), fall back to trashing via dataSources.update, then + * databases.update. + */ +async function archiveEntity( + client: ReturnType, + uuid: string, +): Promise<{ result: unknown; kind: 'page' | 'data_source' | 'database' }> { + try { + const result = await client.pages.update({ + page_id: uuid, + in_trash: true, + }); + return { result, kind: 'page' }; + } catch { + // Not a page — try as data source + } + + try { + const result = await client.dataSources.update({ + data_source_id: uuid, + in_trash: true, + }); + return { result, kind: 'data_source' }; + } catch { + // Not a data source — try as database + } + + const result = await client.databases.update({ + database_id: uuid, + in_trash: true, + }); + return { result, kind: 'database' }; +} + export function archiveCommand(): Command { const cmd = new Command('archive'); cmd - .description('archive (trash) a Notion page') - .argument('', 'Notion page ID or URL') + .description('archive (trash) a Notion page or database') + .argument('', 'Notion page or database ID/URL') .action( withErrorHandling(async (idOrUrl: string) => { const { token, source } = await resolveToken(); @@ -21,16 +57,14 @@ export function archiveCommand(): Command { const id = parseNotionId(idOrUrl); const uuid = toUuid(id); - const updatedPage = await client.pages.update({ - page_id: uuid, - archived: true, - }); + const { result, kind } = await archiveEntity(client, uuid); const mode = getOutputMode(); if (mode === 'json') { - process.stdout.write(`${formatJSON(updatedPage)}\n`); + process.stdout.write(`${formatJSON(result)}\n`); } else { - process.stdout.write('Page archived.\n'); + const label = kind === 'page' ? 'Page' : 'Database'; + process.stdout.write(`${label} archived.\n`); } }), ); diff --git a/src/commands/ls.ts b/src/commands/ls.ts index 7c0c04a..6db699f 100644 --- a/src/commands/ls.ts +++ b/src/commands/ls.ts @@ -7,7 +7,7 @@ import { Command } from 'commander'; import { resolveToken } from '../config/token.js'; import { withErrorHandling } from '../errors/error-handler.js'; import { createNotionClient } from '../notion/client.js'; -import { printOutput, setOutputMode } from '../output/format.js'; +import { getOutputMode, printOutput, setOutputMode } from '../output/format.js'; import { reportTokenSource } from '../output/stderr.js'; function getTitle(item: PageObjectResponse | DataSourceObjectResponse): string { @@ -101,7 +101,11 @@ export function lsCommand(): Command { } if (items.length === 0) { - process.stdout.write('No accessible content found\n'); + if (getOutputMode() === 'json') { + process.stdout.write('[]\n'); + } else { + process.stdout.write('No accessible content found\n'); + } return; } diff --git a/src/commands/search.ts b/src/commands/search.ts index 9cc1d31..db56638 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -7,7 +7,7 @@ import { Command } from 'commander'; import { resolveToken } from '../config/token.js'; import { withErrorHandling } from '../errors/error-handler.js'; import { createNotionClient } from '../notion/client.js'; -import { printOutput, setOutputMode } from '../output/format.js'; +import { getOutputMode, printOutput, setOutputMode } from '../output/format.js'; import { reportTokenSource } from '../output/stderr.js'; function getTitle(item: PageObjectResponse | DataSourceObjectResponse): string { @@ -105,7 +105,11 @@ export function searchCommand(): Command { ) as (PageObjectResponse | DataSourceObjectResponse)[]; if (fullResults.length === 0) { - process.stdout.write(`No results found for "${query}"\n`); + if (getOutputMode() === 'json') { + process.stdout.write('[]\n'); + } else { + process.stdout.write(`No results found for "${query}"\n`); + } return; } diff --git a/src/services/database.service.ts b/src/services/database.service.ts index 0bd29ce..b709f25 100644 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -48,13 +48,16 @@ export async function fetchDatabaseSchema( client: Client, dbId: string, ): Promise { - // In Notion SDK v5, databases are exposed as "data sources" - // client.dataSources.retrieve() returns DataSourceObjectResponse with .properties - const ds = await client.dataSources.retrieve({ data_source_id: dbId }); + // Resolve the data source ID — the input may be a data source ID or a database page ID. + // resolveDataSourceId handles the fallback (try as data source, then as database). + const resolvedId = await resolveDataSourceId(client, dbId); + const ds = await client.dataSources.retrieve({ data_source_id: resolvedId }); // Only full data sources have title and properties const title = - 'title' in ds ? ds.title.map((rt) => rt.plain_text).join('') || dbId : dbId; + 'title' in ds + ? ds.title.map((rt) => rt.plain_text).join('') || resolvedId + : resolvedId; const properties: Record = {}; @@ -85,9 +88,9 @@ export async function fetchDatabaseSchema( typeof ds.parent === 'object' && 'database_id' in ds.parent ? (ds.parent as { database_id: string }).database_id - : dbId; + : resolvedId; - return { id: dbId, databaseId, title, properties }; + return { id: resolvedId, databaseId, title, properties }; } export async function queryDatabase( diff --git a/tests/commands/archive.test.ts b/tests/commands/archive.test.ts index 9300ecd..aae1679 100644 --- a/tests/commands/archive.test.ts +++ b/tests/commands/archive.test.ts @@ -54,13 +54,13 @@ describe('archiveCommand', () => { setOutputMode('auto'); }); - it('calls client.pages.update with archived: true', async () => { + it('calls client.pages.update with in_trash: true', async () => { const cmd = archiveCommand(); await cmd.parseAsync(['node', 'test', 'b55c9c91384d452b81dbd1ef79372b75']); expect(mockPagesUpdate).toHaveBeenCalledWith({ page_id: 'b55c9c91-384d-452b-81db-d1ef79372b75', - archived: true, + in_trash: true, }); });