0.2.2 • Published 2 years ago

@jitl/notion-api v0.2.2

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
2 years ago

@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 | Full API documentation | NPM Package

CMS

The CMS class 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 property suitable for use in a URL.
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.

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.

iteratePaginatedAPI

Dealing with pagination is annoying, but necessary to avoid resource consumption.

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.

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:

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:

// 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:

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:

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:

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:

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.

Stability & Support

API stability: This library follows SemVer, and currently has a version less that 1.0.0, meaning it is under initial development. 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 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.