UNPKG

@coastal-programs/notion-cli

Version:

Unofficial Notion CLI optimized for automation and AI agents. Non-interactive interface for Notion API v5.2.1 with intelligent caching, retry logic, structured error handling, and comprehensive testing.

462 lines (461 loc) 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mapPageStructure = exports.retrievePageRecursive = exports.CircuitBreaker = exports.enhancedFetchWithRetry = exports.cacheManager = exports.search = exports.searchDb = exports.botUser = exports.listUser = exports.retrieveUser = exports.deleteBlock = exports.appendBlockChildren = exports.retrieveBlockChildren = exports.updateBlock = exports.retrieveBlock = exports.updatePage = exports.updatePageProps = exports.createPage = exports.retrievePageProperty = exports.retrievePage = exports.updateDataSource = exports.retrieveDataSource = exports.retrieveDb = exports.updateDb = exports.createDb = exports.fetchAllPagesInDS = exports.fetchWithRetry = exports.client = void 0; const client_1 = require("@notionhq/client"); const cache_1 = require("./cache"); const retry_1 = require("./retry"); exports.client = new client_1.Client({ auth: process.env.NOTION_TOKEN, logLevel: process.env.DEBUG ? client_1.LogLevel.DEBUG : null, }); /** * Legacy fetchWithRetry for backward compatibility * @deprecated Use the enhanced retry logic from retry.ts */ const fetchWithRetry = async (fn, retries = 3) => { return (0, retry_1.fetchWithRetry)(fn, { config: { maxRetries: retries }, }); }; exports.fetchWithRetry = fetchWithRetry; /** * Cached wrapper for API calls with retry logic */ async function cachedFetch(cacheType, cacheKey, fetchFn, options = {}) { const { cacheTtl, skipCache = false, retryConfig } = options; // Check cache first (unless skipped or cache disabled) if (!skipCache) { const cached = cache_1.cacheManager.get(cacheType, cacheKey); if (cached !== null) { if (process.env.DEBUG) { console.log(`Cache HIT: ${cacheType}:${cacheKey}`); } return cached; } if (process.env.DEBUG) { console.log(`Cache MISS: ${cacheType}:${cacheKey}`); } } // Fetch with retry logic const data = await (0, retry_1.fetchWithRetry)(fetchFn, { config: retryConfig, context: `${cacheType}:${cacheKey}`, }); // Store in cache if (!skipCache) { cache_1.cacheManager.set(cacheType, data, cacheTtl, cacheKey); } return data; } /** * Fetch all pages in a data source with pagination */ const fetchAllPagesInDS = async (databaseId, filter) => { const f = filter; const pages = []; let cursor = undefined; while (true) { const { results, next_cursor } = await (0, retry_1.fetchWithRetry)(() => exports.client.dataSources.query({ data_source_id: databaseId, filter: f, start_cursor: cursor, }), { context: `fetchAllPagesInDS:${databaseId}` }); pages.push(...results); if (!next_cursor) { break; } cursor = next_cursor; } return pages; }; exports.fetchAllPagesInDS = fetchAllPagesInDS; /** * Create a database */ const createDb = async (dbProps) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.databases.create(dbProps), { context: 'createDb' }); // Invalidate database list cache cache_1.cacheManager.invalidate('search'); return result; }; exports.createDb = createDb; /** * Update a database */ const updateDb = async (dbProps) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.databases.update(dbProps), { context: `updateDb:${dbProps.database_id}` }); // Invalidate this database's cache cache_1.cacheManager.invalidate('database', dbProps.database_id); cache_1.cacheManager.invalidate('dataSource', dbProps.database_id); return result; }; exports.updateDb = updateDb; /** * Retrieve a database (cached) */ const retrieveDb = async (databaseId) => { return cachedFetch('database', databaseId, () => exports.client.databases.retrieve({ database_id: databaseId })); }; exports.retrieveDb = retrieveDb; /** * Retrieve a data source (cached) */ const retrieveDataSource = async (dataSourceId) => { return cachedFetch('dataSource', dataSourceId, () => exports.client.dataSources.retrieve({ data_source_id: dataSourceId })); }; exports.retrieveDataSource = retrieveDataSource; /** * Update a data source */ const updateDataSource = async (dsProps) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.dataSources.update(dsProps), { context: `updateDataSource:${dsProps.data_source_id}` }); // Invalidate this data source's cache cache_1.cacheManager.invalidate('dataSource', dsProps.data_source_id); return result; }; exports.updateDataSource = updateDataSource; /** * Retrieve a page (cached with short TTL) */ const retrievePage = async (pageProp) => { return cachedFetch('page', pageProp.page_id, () => exports.client.pages.retrieve(pageProp)); }; exports.retrievePage = retrievePage; /** * Retrieve page property */ const retrievePageProperty = async (pageId, propId) => { return (0, retry_1.fetchWithRetry)(() => exports.client.pages.properties.retrieve({ page_id: pageId, property_id: propId, }), { context: `retrievePageProperty:${pageId}:${propId}` }); }; exports.retrievePageProperty = retrievePageProperty; /** * Create a page */ const createPage = async (pageProps) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.pages.create(pageProps), { context: 'createPage' }); // Invalidate parent database/page cache if ('parent' in pageProps && 'database_id' in pageProps.parent) { cache_1.cacheManager.invalidate('dataSource', pageProps.parent.database_id); } return result; }; exports.createPage = createPage; /** * Update page properties */ const updatePageProps = async (pageParams) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.pages.update(pageParams), { context: `updatePageProps:${pageParams.page_id}` }); // Invalidate this page's cache cache_1.cacheManager.invalidate('page', pageParams.page_id); return result; }; exports.updatePageProps = updatePageProps; /** * Update page content by replacing all blocks * To keep the same page URL, remove all blocks in the page and add new blocks */ const updatePage = async (pageId, blocks) => { // Get all blocks const blks = await (0, retry_1.fetchWithRetry)(() => exports.client.blocks.children.list({ block_id: pageId }), { context: `updatePage:list:${pageId}` }); // Delete all blocks for (const blk of blks.results) { await (0, retry_1.fetchWithRetry)(() => exports.client.blocks.delete({ block_id: blk.id }), { context: `updatePage:delete:${blk.id}` }); } // Append new blocks const res = await (0, retry_1.fetchWithRetry)(() => exports.client.blocks.children.append({ block_id: pageId, children: blocks, }), { context: `updatePage:append:${pageId}` }); // Invalidate caches cache_1.cacheManager.invalidate('page', pageId); cache_1.cacheManager.invalidate('block', pageId); return res; }; exports.updatePage = updatePage; /** * Retrieve a block (cached with very short TTL) */ const retrieveBlock = async (blockId) => { return cachedFetch('block', blockId, () => exports.client.blocks.retrieve({ block_id: blockId })); }; exports.retrieveBlock = retrieveBlock; /** * Update a block */ const updateBlock = async (params) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.blocks.update(params), { context: `updateBlock:${params.block_id}` }); // Invalidate this block's cache cache_1.cacheManager.invalidate('block', params.block_id); return result; }; exports.updateBlock = updateBlock; /** * Retrieve block children (cached with very short TTL) */ const retrieveBlockChildren = async (blockId) => { return cachedFetch('block', `${blockId}:children`, () => exports.client.blocks.children.list({ block_id: blockId })); }; exports.retrieveBlockChildren = retrieveBlockChildren; /** * Append block children */ const appendBlockChildren = async (params) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.blocks.children.append(params), { context: `appendBlockChildren:${params.block_id}` }); // Invalidate parent block's cache cache_1.cacheManager.invalidate('block', params.block_id); cache_1.cacheManager.invalidate('block', `${params.block_id}:children`); return result; }; exports.appendBlockChildren = appendBlockChildren; /** * Delete a block */ const deleteBlock = async (blockId) => { const result = await (0, retry_1.fetchWithRetry)(() => exports.client.blocks.delete({ block_id: blockId }), { context: `deleteBlock:${blockId}` }); // Invalidate this block's cache cache_1.cacheManager.invalidate('block', blockId); return result; }; exports.deleteBlock = deleteBlock; /** * Retrieve a user (cached with long TTL) */ const retrieveUser = async (userId) => { return cachedFetch('user', userId, () => exports.client.users.retrieve({ user_id: userId })); }; exports.retrieveUser = retrieveUser; /** * List all users (cached with long TTL) */ const listUser = async () => { return cachedFetch('user', 'list', () => exports.client.users.list({})); }; exports.listUser = listUser; /** * Get bot user info (cached with long TTL) */ const botUser = async () => { return cachedFetch('user', 'me', () => exports.client.users.me({})); }; exports.botUser = botUser; /** * Search for databases (cached with medium TTL) */ const searchDb = async () => { const { results } = await cachedFetch('search', 'databases', async () => { return await exports.client.search({ filter: { value: 'data_source', property: 'object', }, }); }); return results; }; exports.searchDb = searchDb; /** * General search (not cached due to variable parameters) */ const search = async (params) => { return (0, retry_1.fetchWithRetry)(() => exports.client.search(params), { context: 'search' }); }; exports.search = search; /** * Export cache manager for external use */ var cache_2 = require("./cache"); Object.defineProperty(exports, "cacheManager", { enumerable: true, get: function () { return cache_2.cacheManager; } }); /** * Export retry utilities for external use */ var retry_2 = require("./retry"); Object.defineProperty(exports, "enhancedFetchWithRetry", { enumerable: true, get: function () { return retry_2.fetchWithRetry; } }); Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return retry_2.CircuitBreaker; } }); /** * Recursively retrieve a page with all its blocks and nested content * @param pageId - The ID of the page to retrieve * @param depth - Current recursion depth (internal use) * @param maxDepth - Maximum depth to recurse (default: 3) * @returns Object containing page metadata, blocks, and optional warnings */ const retrievePageRecursive = async (pageId, depth = 0, maxDepth = 3) => { var _a, _b; // Prevent infinite recursion if (depth >= maxDepth) { return { page: null, blocks: [], warnings: [ { block_id: pageId, type: 'max_depth_reached', message: `Maximum recursion depth of ${maxDepth} reached`, has_children: false, }, ], }; } // Retrieve the page const page = await (0, exports.retrievePage)({ page_id: pageId }); // Retrieve all blocks (children) const blocksResponse = await (0, exports.retrieveBlockChildren)(pageId); const blocks = blocksResponse.results || []; const warnings = []; // Recursively fetch nested blocks for (const block of blocks) { // Skip partial blocks if (!(0, client_1.isFullBlock)(block)) { continue; } // Handle unsupported blocks if (block.type === 'unsupported') { warnings.push({ block_id: block.id, type: 'unsupported', notion_type: ((_a = block.unsupported) === null || _a === void 0 ? void 0 : _a.type) || 'unknown', message: `Block type '${((_b = block.unsupported) === null || _b === void 0 ? void 0 : _b.type) || 'unknown'}' not supported by Notion API`, has_children: block.has_children, }); continue; } // Recursively fetch children for blocks that have them if (block.has_children) { try { const childrenResponse = await (0, exports.retrieveBlockChildren)(block.id); block.children = childrenResponse.results || []; // If this is a child_page block, recursively fetch that page too if (block.type === 'child_page' && depth + 1 < maxDepth) { const childPageData = await (0, exports.retrievePageRecursive)(block.id, depth + 1, maxDepth); block.child_page_details = childPageData; // Merge warnings from recursive calls if (childPageData.warnings) { warnings.push(...childPageData.warnings); } } } catch (error) { // If we can't fetch children, add a warning warnings.push({ block_id: block.id, type: 'fetch_error', message: `Failed to fetch children for block: ${error instanceof Error ? error.message : 'Unknown error'}`, has_children: true, }); } } } return { page, blocks, ...(warnings.length > 0 && { warnings }), }; }; exports.retrievePageRecursive = retrievePageRecursive; /** * Map page structure (fast page discovery with parallel fetching) * Returns minimal structure info (titles, types, IDs) instead of full content * @param pageId - The ID of the page to map * @returns Object containing page ID, title, icon, and structure overview */ const mapPageStructure = async (pageId) => { // Parallel fetch: get page and blocks simultaneously const [page, blocksResponse] = await Promise.all([ (0, exports.retrievePage)({ page_id: pageId }), (0, exports.retrieveBlockChildren)(pageId), ]); const blocks = blocksResponse.results || []; // Extract page title let pageTitle = 'Untitled'; if (page.object === 'page' && (0, client_1.isFullPage)(page)) { Object.entries(page.properties).find(([, prop]) => { if (prop.type === 'title' && prop.title.length > 0) { pageTitle = prop.title[0].plain_text; return true; } return false; }); } // Extract page icon let pageIcon; if ((0, client_1.isFullPage)(page) && page.icon) { if (page.icon.type === 'emoji') { pageIcon = page.icon.emoji; } else if (page.icon.type === 'external') { pageIcon = page.icon.external.url; } else if (page.icon.type === 'file') { pageIcon = page.icon.file.url; } } // Build minimal structure const structure = blocks.map((block) => { const structureItem = { type: block.type, id: block.id, }; // Extract title/text based on block type try { switch (block.type) { case 'child_page': structureItem.title = block[block.type].title; break; case 'child_database': structureItem.title = block[block.type].title; break; case 'heading_1': case 'heading_2': case 'heading_3': case 'paragraph': case 'bulleted_list_item': case 'numbered_list_item': case 'to_do': case 'toggle': case 'quote': case 'callout': case 'code': if (block[block.type].rich_text && block[block.type].rich_text.length > 0) { structureItem.text = block[block.type].rich_text[0].plain_text; } break; case 'bookmark': case 'embed': case 'link_preview': structureItem.text = block[block.type].url; break; case 'equation': structureItem.text = block[block.type].expression; break; case 'image': case 'file': case 'video': case 'pdf': if (block[block.type].type === 'file') { structureItem.text = block[block.type].file.url; } else if (block[block.type].type === 'external') { structureItem.text = block[block.type].external.url; } break; // For other types, just include type and id default: break; } } catch { // If extraction fails, just include type and id } return structureItem; }); return { id: pageId, title: pageTitle, type: 'page', ...(pageIcon && { icon: pageIcon }), structure, }; }; exports.mapPageStructure = mapPageStructure;