UNPKG

@jitl/notion-api

Version:

The missing companion library for the official Notion public API.

231 lines (185 loc) 8.15 kB
# @jitl/notion-api The missing companion library for the official Notion public API. - Use Notion as a headless content management system a la Contentful. - Recursively fetch page content while building backlinks. - Convenient types like `Page` `Block` ..., plus helpers for tasks like iterating paginated API results. - Image, emoji, and content caching specifically designed for NextJS and incremental static regeneration. **This is not an official Notion product**. The current focus of this library is on _reading_ data from Notion. [Github](https://github.com/justjake/monorepo/tree/main/packages/notion-api) | [Full API documentation](https://github.com/justjake/monorepo/blob/main/packages/notion-api/doc/modules.md) | [NPM Package](https://www.npmjs.com/package/@jitl/notion-api) ## CMS The [CMS class](./doc/classes/CMS.md) is a wrapper around a Notion database. A CMS instance adds the following features: - Page content fetching and caching. Calling CMS methods to retrieve pages from the Notion API will only re-fetch the contents of the page if the page has been updated. Cached page content can optionally be persisted to disk as JSON files. - Optional cover image, icon and image block asset download, including images for unicode emojis. - Automatically derive a metadata object called `frontmatter` for each page, to reduce page property parsing boilerplate, and provide a type-safe API for your pages to the rest of your app. - Support for retrieving pages by a special [`slug`](./doc/interfaces/CMSConfig.md#slug) property suitable for use in a URL. ```typescript import { NotionClient, // re-exported official Notion client from peer dependencies NotionClientDebugLogger, // enable logs with DEBUG='@jitl/notion-api:*' CMS, richTextAsPlainText, } from '@jitl/notion-api'; const Recipes = new CMS({ database_id: 'a3aa29a6b2f242d1b4cf86fb578a5eea', notion: new NotionClient({ logger: NotionClientDebugLogger, auth: process.env.NOTION_SECRET, }), slug: undefined, // Use page ID visible: true, // All pages visible getFrontmatter: (page) => ({ /* TODO: return your custom metadata */ }), cache: { directory: path.join(__dirname, './cache'), }, assets: { directory: path.join(__dirname, './assets'), downloadExternalAssets: true, }, }); // Download and cache all pages in the Recipes database, and their assets. for await (const recipe of Recipes.query()) { console.log( 'Downloading assets for recipe: ', richTextAsPlainText(recipe.frontmatter.title) ); await Recipes.downloadAssets(recipe); } ``` ## API Types & Helpers This library exports many type aliases for working with data retrieved from the [official `@notionhq/client` library](https://github.com/makenotion/notion-sdk-js). These types are derived from the official library's publicly exported types. They will be compatible with @notionhq/client, but may change in unexpected ways after a @notionhq/client update. Abbreviated list of types: `Block<BlockType>`, `Page`, `RichText`, `RichTextToken`, `Mention<MentionType>`, `Property`, `PropertyFilter`, `User`, etc. There are several handy utility functions for working with those types, like `richTextAsPlainText(text)` and `getPropertyValue(page, propertyPointer)`. See the full list in [the API documentation](./doc/modules.md). ### iteratePaginatedAPI Dealing with pagination is annoying, but [necessary to avoid resource consumption](https://www.notion.so/blog/creating-the-notion-api#:~:text=Paginating%20block%20hierarchies). The `iteratePaginatedAPI` helper returns an `AsyncIterable<Item>` so you can iterate over Notion API results using the `for await (...) { ... }` syntax. This should work for any paginated API using Notion's official API client. ```typescript for await (const block of iteratePaginatedAPI(notion.blocks.children.list, { block_id: parentBlockId, })) { // Do something with block. } ``` If you prefer a function approach and don't mind waiting for all values to load into memory, consider `asyncIterableToArray`: ```typescript const iterator = iteratePaginatedAPI(notion.blocks.children.list, { block_id: parentBlockId, }); const blocks = await asyncIterableToArray(iterator); const paragraphs = blocks.filter((block) => isFullBlock(block, 'paragraph')); ``` ### Partial response types The Notion API can sometimes return "partial" object data that contain only the block's ID: ```typescript // In @notionhq/client typings: type PartialBlockObjectResponse = { object: 'block'; id: string }; export type GetBlockResponse = PartialBlockObjectResponse | BlockObjectResponse; ``` Checking that a `GetBlockResponse` (or similar type) is a "full" block gets old pretty fast, so this library exports type guard functions to handle common cases, like `isFullPage(page)` and `isFullBlock(block)`. `isFullBlock` can optionally narrow the _type_ of block as well: ```typescript if (isFullBlock(block, 'paragraph')) { // It's a full paragraph block console.log(richTextAsPlainText(block.paragraph.text)); } ``` ### Block data Notion's API returns block data in a shape that is very difficult to deal with in a generic way while maintaining type-safety. Each block type has it's own property with the same name, and that property contains the block's data. Handling this type-safely means writing a long and annoying switch statement: ```typescript function getBlockTextContentBefore(block: Block): RichText | RichText[] { switch (block.type) { case 'paragraph': return block.paragraph.rich_text; case 'heading_1': return block.heading_1.rich_text; case 'heading_2': return block.heading_2.rich_text; // ... etc, for many more block types default: assertUnreachable(block); // Assert this switch is exhaustive } } ``` Enter `getBlockData`. It returns a union of all possible interior data types for a `block` value. The same function can be re-written in a type-safe but non-exhaustive way in much fewer lines: ```typescript function getBlockTextContentAfter(block: Block): RichText[] { const blockData = getBlockData(block); const results: RichText[] = []; if ('rich_text' in blockData) { results.push(blockData.rich_text); } if ('caption' in blockData) { results.push(blockData.caption); } // Done. return results; } ``` But because this function supports narrowed block types, you can still use a `switch (block.type)` if you want to be exhaustive, and tab completion will guide you: ```typescript function getBlockTextContentAfterExhaustive( block: Block ): RichText | RichText[] { switch (block.type) { case 'paragraph': // Fall-through for blocks with only rich_text case 'heading_1': case 'heading_2': // ... etc return getBlockData(block).rich_text; case 'image': return getBlockData(block).caption; case 'code': return [getBlockData(block).rich_text, getBlockData(block).caption]; // ... etc default: assertUnreachable(block); // Assert this switch is exhaustive } } ``` See the full list of functions in [the API documentation](./doc/modules.md). ## Stability & Support **API stability**: This library follows SemVer, and currently has a version less that 1.0.0, meaning [it is under initial development](https://semver.org/#spec-item-4). Do not expect API stability between versions, so specify an exact version in `package.json` or use a lockfile (`package-lock.json`, `yarn.loc` etc) to protect yourself from unexpected breaking changes. **Support**: As stated above, **this library not an official Notion product**. I wrote it for my own use, to support [my website](https://jake.tl) and other projects, although I welcome contributions of any kind. There are no automated tests yet. **TypeScript**: This library is developed with TypeScript 4.5.5, and is untested with other TypeScript versions. ## Development ### Monorepo This library is developed inside a monorepo, please see the root README.md for more information. ### Running unit tests Run `nx test notion-api` to execute the unit tests via [Jest](https://jestjs.io).