diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 00000000..558e81a1 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,29 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.1 + "version": "0.1", + // language - current active spelling language + "language": "en", + // words - list of words to be always considered correct + "words": [ + "notionhq", + "strikethrough", + "makenotion", + "sendgrid", + "blackmad", + "octokit", + "printf", + "is_toggleable" + ], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + // For example "hte" should be "the" + "flagWords": ["hte"], + "ignorePaths": [ + "package.json", + "package-lock.json", + "node_modules/**", + "examples/**/node_modules/**", + "build/**" + ] +} diff --git a/.eslintrc.js b/.eslintrc.js index 351664bc..f9e5a1b1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,13 +2,29 @@ module.exports = { root: true, - parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint' - ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended' + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", ], -}; + env: { + node: true, + commonjs: true, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + // Allow assertion types. + varsIgnorePattern: "^_assert", + caughtErrors: "none", + ignoreRestSiblings: true, + }, + ], + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + }, +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..8b999618 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +Report bugs here only for the Node JavaScript library. + +If you're having problems using Notion's API, or have any other feedback about the API including feature requests for the JavaScript library, please email support at developers@makenotion.com. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Node version: +Notion JS library version: + +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +Please include any screenshots that help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc439f10..7f697b08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,25 +5,25 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: build-and-test: - runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x, 15.x] + node-version: [12.x, 14.x, 15.x, 16.x] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm run build --if-present - - run: npm test + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build + - run: npm run lint + - run: npm test diff --git a/.gitignore b/.gitignore index a50ee079..0bff56cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ -/node_modules +node_modules package-lock.json /build + +/examples/**/node_modules +/examples/**/package-lock.json +/examples/**/.env + +.vscode + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..2c32f105 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +build +.github/**/*.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..a6cb889c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "arrowParens": "avoid", + "semi": false, + "trailingComma": "es5", + "endOfLine": "lf" +} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 2f4dc438..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cSpell.words": [ - "notionhq" - ] -} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..12dd7d86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 Notion Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 9a813a79..a1727b81 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,285 @@ -# Notion SDK for JavaScript +
+

Notion SDK for JavaScript

+

+ A simple and easy to use client for the Notion API +

+
+
-**TODO** +![Build status](https://github.com/makenotion/notion-sdk-js/actions/workflows/ci.yml/badge.svg) +[![npm version](https://badge.fury.io/js/%40notionhq%2Fclient.svg)](https://www.npmjs.com/package/@notionhq/client) +## Installation +``` +npm install @notionhq/client +``` + +## Usage + +> Use Notion's [Getting Started Guide](https://developers.notion.com/docs/getting-started) to get set up to use Notion's API. + +Import and initialize a client using an **integration token** or an OAuth **access token**. + +```js +const { Client } = require("@notionhq/client") + +// Initializing a client +const notion = new Client({ + auth: process.env.NOTION_TOKEN, +}) +``` + +Make a request to any Notion API endpoint. + +> See the complete list of endpoints in the [API reference](https://developers.notion.com/reference). + +```js +;(async () => { + const listUsersResponse = await notion.users.list({}) +})() +``` + +Each method returns a `Promise` which resolves the response. + +```js +console.log(listUsersResponse) +``` + +``` +{ + results: [ + { + object: 'user', + id: 'd40e767c-d7af-4b18-a86d-55c61f1e39a4', + type: 'person', + person: { + email: 'avo@example.org', + }, + name: 'Avocado Lovelace', + avatar_url: 'https://secure.notion-static.com/e6a352a8-8381-44d0-a1dc-9ed80e62b53d.jpg', + }, + ... + ] +} +``` + +Endpoint parameters are grouped into a single object. You don't need to remember which parameters go in the path, query, or body. + +```js +const myPage = await notion.databases.query({ + database_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af", + filter: { + property: "Landmark", + rich_text: { + contains: "Bridge", + }, + }, +}) +``` + +### Handling errors + +If the API returns an unsuccessful response, the returned `Promise` rejects with a `APIResponseError`. + +The error contains properties from the response, and the most helpful is `code`. You can compare `code` to the values in the `APIErrorCode` object to avoid misspelling error codes. + +```js +const { Client, APIErrorCode } = require("@notionhq/client") + +try { + const notion = new Client({ auth: process.env.NOTION_TOKEN }) + const myPage = await notion.databases.query({ + database_id: databaseId, + filter: { + property: "Landmark", + rich_text: { + contains: "Bridge", + }, + }, + }) +} catch (error) { + if (error.code === APIErrorCode.ObjectNotFound) { + // + // For example: handle by asking the user to select a different database + // + } else { + // Other error handling code + console.error(error) + } +} +``` + +### Logging + +The client emits useful information to a logger. By default, it only emits warnings and errors. + +If you're debugging an application, and would like the client to log response bodies, set the `logLevel` option to `LogLevel.DEBUG`. + +```js +const { Client, LogLevel } = require("@notionhq/client") + +const notion = new Client({ + auth: process.env.NOTION_TOKEN, + logLevel: LogLevel.DEBUG, +}) +``` + +You may also set a custom `logger` to emit logs to a destination other than `stdout`. A custom logger is a function which is called with 3 parameters: `logLevel`, `message`, and `extraInfo`. The custom logger should not return a value. + +### Client options + +The `Client` supports the following options on initialization. These options are all keys in the single constructor parameter. + +| Option | Default value | Type | Description | +| ----------- | -------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `auth` | `undefined` | `string` | Bearer token for authentication. If left undefined, the `auth` parameter should be set on each request. | +| `logLevel` | `LogLevel.WARN` | `LogLevel` | Verbosity of logs the instance will produce. By default, logs are written to `stdout`. | +| `timeoutMs` | `60_000` | `number` | Number of milliseconds to wait before emitting a `RequestTimeoutError` | +| `baseUrl` | `"https://api.notion.com"` | `string` | The root URL for sending API requests. This can be changed to test with a mock server. | +| `logger` | Log to console | `Logger` | A custom logging function. This function is only called when the client emits a log that is equal or greater severity than `logLevel`. | +| `agent` | Default node agent | `http.Agent` | Used to control creation of TCP sockets. A common use is to proxy requests with [`https-proxy-agent`](https://github.com/TooTallNate/node-https-proxy-agent) | + +### TypeScript + +This package contains type definitions for all request parameters and responses, +as well as some useful sub-objects from those entities. + +Because errors in TypeScript start with type `any` or `unknown`, you should use +the `isNotionClientError` type guard to handle them in a type-safe way. Each +`NotionClientError` type is uniquely identified by its `error.code`. Codes in +the `APIErrorCode` enum are returned from the server. Codes in the +`ClientErrorCode` enum are produced on the client. + +```ts +try { + const response = await notion.databases.query({ + /* ... */ + }) +} catch (error: unknown) { + if (isNotionClientError(error)) { + // error is now strongly typed to NotionClientError + switch (error.code) { + case ClientErrorCode.RequestTimeout: + // ... + break + case APIErrorCode.ObjectNotFound: + // ... + break + case APIErrorCode.Unauthorized: + // ... + break + // ... + default: + // you could even take advantage of exhaustiveness checking + assertNever(error.code) + } + } +} +``` + +#### Type guards + +There are several [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) +provided to distinguish between full and partial API responses. + +| Type guard function | Purpose | +| ---------------------- | -------------------------------------------------------------------------------------- | +| `isFullPage` | Determine whether an object is a full `PageObjectResponse` | +| `isFullBlock` | Determine whether an object is a full `BlockObjectResponse` | +| `isFullDatabase` | Determine whether an object is a full `DatabaseObjectResponse` | +| `isFullPageOrDatabase` | Determine whether an object is a full `PageObjectResponse` or `DatabaseObjectResponse` | +| `isFullUser` | Determine whether an object is a full `UserObjectResponse` | +| `isFullComment` | Determine whether an object is a full `CommentObjectResponse` | + +Here is an example of using a type guard: + +```typescript +const fullOrPartialPages = await notion.databases.query({ + database_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af", +}) +for (const page of fullOrPartialPages.results) { + if (!isFullPageOrDatabase(page)) { + continue + } + // The page variable has been narrowed from + // PageObjectResponse | PartialPageObjectResponse | DatabaseObjectResponse | PartialDatabaseObjectResponse + // to + // PageObjectResponse | DatabaseObjectResponse. + console.log("Created at:", page.created_time) +} +``` + +### Utility functions + +This package also exports a few utility functions that are helpful for dealing with +any of our paginated APIs. + +#### `iteratePaginatedAPI(listFn, firstPageArgs)` + +This utility turns any paginated API into an async iterator. + +**Parameters:** + +- `listFn`: Any function on the Notion client that represents a paginated API (i.e. accepts + `start_cursor`.) Example: `notion.blocks.children.list`. +- `firstPageArgs`: Arguments that should be passed to the API on the first and subsequent calls + to the API, for example a `block_id`. + +**Returns:** + +An [async iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols) +over results from the API. + +**Example:** + +```javascript +for await (const block of iteratePaginatedAPI(notion.blocks.children.list, { + block_id: parentBlockId, +})) { + // Do something with block. +} +``` + +#### `collectPaginatedAPI(listFn, firstPageArgs)` + +This utility accepts the same arguments as `iteratePaginatedAPI`, but collects +the results into an in-memory array. + +Before using this utility, check that the data you are dealing with is +small enough to fit in memory. + +**Parameters:** + +- `listFn`: Any function on the Notion client that represents a paginated API (i.e. accepts + `start_cursor`.) Example: `notion.blocks.children.list`. +- `firstPageArgs`: Arguments that should be passed to the API on the first and subsequent calls + to the API, for example a `block_id`. + +**Returns:** + +An array with results from the API. + +**Example:** + +```javascript +const blocks = await collectPaginatedAPI(notion.blocks.children.list, { + block_id: parentBlockId, +}) +// Do something with blocks. +``` + +## Requirements + +This package supports the following minimum versions: + +- Runtime: `node >= 12` +- Type definitions (optional): `typescript >= 4.5` + +Earlier versions may still work, but we encourage people building new applications to upgrade to the current stable. + +## Getting help + +If you want to submit a feature request for Notion's API, or are experiencing any issues with the API platform, please email us at `developers@makenotion.com`. + +To report issues with the SDK, it is possible to [submit an issue](https://github.com/makenotion/notion-sdk-js/issues) to this repo. However, we don't monitor these issues very closely. We recommend you reach out to us at `developers@makenotion.com` instead. diff --git a/ava.config.js b/ava.config.js deleted file mode 100644 index 8ce7dc70..00000000 --- a/ava.config.js +++ /dev/null @@ -1,9 +0,0 @@ -export default { - typescript: { - rewritePaths: { - 'src/': 'build/src/', - 'test/': 'build/test/' - }, - compile: 'tsc', - }, -}; diff --git a/examples/database-email-update/.prettierrc b/examples/database-email-update/.prettierrc new file mode 100644 index 00000000..302f65ea --- /dev/null +++ b/examples/database-email-update/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "avoid", + "tabWidth": 2, + "semi": false, + "trailingComma": "es5", + "endOfLine": "lf", + "singleQuote": false +} diff --git a/examples/database-email-update/README.md b/examples/database-email-update/README.md new file mode 100644 index 00000000..ce118a34 --- /dev/null +++ b/examples/database-email-update/README.md @@ -0,0 +1,57 @@ +# Sample Integration: Notion to Email + +drawing + +## About the Integration + +This Notion integration sends an email whenever the Status of a page in a database is updated. + +This sample was built using [this database template](https://public-api-examples.notion.site/0def5dfb6d9b4cdaa907a0466834b9f4?v=aea75fc133e54b3382d12292291d9248) and emails are sent using [SendGrid's API](https://sendgrid.com). + +### 1. Setup your local project + +```zsh +# Clone this repository locally +git clone https://github.com/makenotion/notion-sdk-js.git + +# Switch into this project +cd notion-sdk-js/examples/database-update-send-email + +# Install the dependencies +npm install +``` + +### 2. Setup a free account at [SendGrid](https://sendgrid.com) + +Sign up for a free account and follow the instructions to use the Email API. + +Choose the option for integrating with the Web API and follow instructions to +get your API token. + +## Running Locally + +### 3. Setup your Notion workspace + +You can create your Notion API key [here](https://www.notion.com/my-integrations). + +To create a Notion database that will work with this example, duplicate [this database template](https://public-api-examples.notion.site/0def5dfb6d9b4cdaa907a0466834b9f4?v=aea75fc133e54b3382d12292291d9248). + +Your Notion integration will need access to the Notion database you have created. To provide access, follow the instructions found in Notion's [Integration guide](https://developers.notion.com/docs/create-a-notion-integration#step-2-share-a-database-with-your-integration). + +### 4. Set your environment variables to a `.env` file + +Rename `example.env` to `.env` in this directory and add the following fields: + +```zsh +NOTION_KEY= +SENDGRID_KEY= +NOTION_DATABASE_ID= +EMAIL_TO_FIELD= +EMAIL_FROM_FIELD= +``` + +### 5. Run code + +```zsh +npm run ts-run +``` diff --git a/examples/database-email-update/example.env b/examples/database-email-update/example.env new file mode 100644 index 00000000..c93f6aa0 --- /dev/null +++ b/examples/database-email-update/example.env @@ -0,0 +1,5 @@ +NOTION_KEY= +SENDGRID_KEY= +NOTION_DATABASE_ID= +EMAIL_TO_FIELD= +EMAIL_FROM_FIELD= diff --git a/examples/database-email-update/index.ts b/examples/database-email-update/index.ts new file mode 100644 index 00000000..205722f4 --- /dev/null +++ b/examples/database-email-update/index.ts @@ -0,0 +1,244 @@ +/* ================================================================================ + + database-update-send-email. + + Glitch example: https://glitch.com/edit/#!/notion-database-email-update + Find the official Notion API client @ https://github.com/makenotion/notion-sdk-js/ + +================================================================================ */ + +import { Client } from "@notionhq/client" +import { config } from "dotenv" +import SendGrid from "@sendgrid/mail" +import { PropertyItemObjectResponse } from "../../build/src/api-endpoints" + +config() +SendGrid.setApiKey(process.env.SENDGRID_KEY) +const notion = new Client({ auth: process.env.NOTION_KEY }) + +const databaseId = process.env.NOTION_DATABASE_ID + +/** + * Local map to store task pageId to its last status. + * { [pageId: string]: string } + */ +const taskPageIdToStatusMap = {} + +/** + * Initialize local data store. + * Then poll for changes every 5 seconds (5000 milliseconds). + */ +setInitialTaskPageIdToStatusMap().then(() => { + setInterval(findAndSendEmailsForUpdatedTasks, 5000) +}) + +/** + * Get and set the initial data store with tasks currently in the database. + */ +async function setInitialTaskPageIdToStatusMap() { + const currentTasks = await getTasksFromNotionDatabase() + for (const { pageId, status } of currentTasks) { + taskPageIdToStatusMap[pageId] = status + } +} + +async function findAndSendEmailsForUpdatedTasks() { + // Get the tasks currently in the database. + console.log("\nFetching tasks from Notion DB...") + const currentTasks = await getTasksFromNotionDatabase() + + // Return any tasks that have had their status updated. + const updatedTasks = findUpdatedTasks(currentTasks) + console.log(`Found ${updatedTasks.length} updated tasks.`) + + // For each updated task, update taskPageIdToStatusMap and send an email notification. + for (const task of updatedTasks) { + taskPageIdToStatusMap[task.pageId] = task.status + await sendUpdateEmailWithSendgrid(task) + } +} + +/** + * Gets tasks from the database. + */ +async function getTasksFromNotionDatabase(): Promise< + Array<{ pageId: string; status: string; title: string }> +> { + const pages = [] + let cursor = undefined + + const shouldContinue = true + while (shouldContinue) { + const { results, next_cursor } = await notion.databases.query({ + database_id: databaseId, + start_cursor: cursor, + }) + pages.push(...results) + if (!next_cursor) { + break + } + cursor = next_cursor + } + console.log(`${pages.length} pages successfully fetched.`) + + const tasks = [] + for (const page of pages) { + const pageId = page.id + + const statusPropertyId = page.properties["Status"].id + const statusPropertyItem = await getPropertyValue({ + pageId, + propertyId: statusPropertyId, + }) + + const status = getStatusPropertyValue(statusPropertyItem) + + const titlePropertyId = page.properties["Name"].id + const titlePropertyItems = await getPropertyValue({ + pageId, + propertyId: titlePropertyId, + }) + const title = getTitlePropertyValue(titlePropertyItems) + + tasks.push({ pageId, status, title }) + } + + return tasks +} + +/** + * Extract status as string from property value + */ +function getStatusPropertyValue( + property: PropertyItemObjectResponse | Array +): string { + if (Array.isArray(property)) { + if (property?.[0]?.type === "select") { + return property[0].select.name + } else { + return "No Status" + } + } else { + if (property.type === "select") { + return property.select.name + } else { + return "No Status" + } + } +} + +/** + * Extract title as string from property value + */ +function getTitlePropertyValue( + property: PropertyItemObjectResponse | Array +): string { + if (Array.isArray(property)) { + if (property?.[0].type === "title") { + return property[0].title.plain_text + } else { + return "No Title" + } + } else { + if (property.type === "title") { + return property.title.plain_text + } else { + return "No Title" + } + } +} + +/** + * Compares task to most recent version of task stored in taskPageIdToStatusMap. + * Returns any tasks that have a different status than their last version. + */ +function findUpdatedTasks( + currentTasks: Array<{ pageId: string; status: string; title: string }> +): Array<{ pageId: string; status: string; title: string }> { + return currentTasks.filter(currentTask => { + const previousStatus = getPreviousTaskStatus(currentTask) + return currentTask.status !== previousStatus + }) +} + +/** + * Sends task update notification using Sendgrid. + */ +async function sendUpdateEmailWithSendgrid({ + title, + status, +}: { + status: string + title: string +}) { + const message = `Status of Notion task ("${title}") has been updated to "${status}".` + console.log(message) + + try { + // Send an email about this change. + await SendGrid.send({ + to: process.env.EMAIL_TO_FIELD, + from: process.env.EMAIL_FROM_FIELD, + subject: "Notion Task Status Updated", + text: message, + }) + console.log( + `Email Sent to ${process.env.EMAIL_TO_FIELD}, from: ${process.env.EMAIL_FROM_FIELD}` + ) + } catch (error) { + console.error(error) + } +} + +/** + * Finds or creates task in local data store and returns its status. + */ +function getPreviousTaskStatus({ pageId, status }): string { + // If this task hasn't been seen before, add to local pageId to status map. + if (!taskPageIdToStatusMap[pageId]) { + taskPageIdToStatusMap[pageId] = status + } + return taskPageIdToStatusMap[pageId] +} + +/** + * If property is paginated, returns an array of property items. + * + * Otherwise, it will return a single property item. + */ +async function getPropertyValue({ + pageId, + propertyId, +}: { + pageId: string + propertyId: string +}): Promise> { + let propertyItem = await notion.pages.properties.retrieve({ + page_id: pageId, + property_id: propertyId, + }) + if (propertyItem.object === "property_item") { + return propertyItem + } + + // Property is paginated. + let nextCursor = propertyItem.next_cursor + const results = propertyItem.results + + while (nextCursor !== null) { + propertyItem = await notion.pages.properties.retrieve({ + page_id: pageId, + property_id: propertyId, + start_cursor: nextCursor, + }) + + if (propertyItem.object === "list") { + nextCursor = propertyItem.next_cursor + results.push(...propertyItem.results) + } else { + nextCursor = null + } + } + + return results +} diff --git a/examples/database-email-update/package.json b/examples/database-email-update/package.json new file mode 100644 index 00000000..f0c97d93 --- /dev/null +++ b/examples/database-email-update/package.json @@ -0,0 +1,21 @@ +{ + "name": "database-update-send-email", + "version": "1.0.0", + "description": "Send an email with SendGrid when an update is made in Notion.", + "main": "index.js", + "engines": { + "node": "12.x" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "ts-run": "node --loader ts-node/esm index.ts" + }, + "dependencies": { + "@notionhq/client": "file:../../", + "@sendgrid/mail": "^7.7.0", + "dotenv": "^16.0.1", + "ts-node": "^10.8.2" + }, + "author": "Aman Gupta", + "license": "MIT" +} diff --git a/examples/database-email-update/tsconfig.json b/examples/database-email-update/tsconfig.json new file mode 100644 index 00000000..85f505fb --- /dev/null +++ b/examples/database-email-update/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "incremental": true, + "sourceMap": false, + + "target": "ESNext", + "module": "commonjs", + + "moduleResolution": "Node", + + "allowSyntheticDefaultImports": true, + + "useDefineForClassFields": false, + + "strict": false, + "suppressImplicitAnyIndexErrors": true, + + "isolatedModules": true, + "esModuleInterop": true, + "noUncheckedIndexedAccess": true, + "allowJs": true, + "checkJs": false + }, + "include": ["src/**/*"] +} diff --git a/examples/generate-random-data/README.md b/examples/generate-random-data/README.md new file mode 100644 index 00000000..0d10bae0 --- /dev/null +++ b/examples/generate-random-data/README.md @@ -0,0 +1,42 @@ +# Sample Integration: Generate Random Data with Notion API + +## About the Integration + +This integration finds the first database which your bot has access to, and creates correctly typed random rows of data. + +## Running Locally + +### 1. Setup your local project + +```zsh +# Clone this repository locally +git clone https://github.com/makenotion/notion-sdk-js.git + +# Switch into this project +cd notion-sdk-js/examples/generate-random-data + +# Install the dependencies +npm install +``` + +### 2. Setup your Notion workspace + +You can create your Notion API key [here](https://www.notion.com/my-integrations). + +To create a Notion database that will work with this example, duplicate [this database template](https://public-api-examples.notion.site/f3e098475baa45878759ed8d04ea79af). + +Your Notion integration will need access to the Notion database you have created. To provide access, follow the instructions found in Notion's [Integration guide](https://developers.notion.com/docs/create-a-notion-integration#step-2-share-a-database-with-your-integration). + +### 3. Set your environment variables to a `.env` file + +Rename `example.env` to `.env` in this directory and add your API key. + +```zsh +NOTION_KEY= +``` + +### 4. Run code + +```zsh +npm run ts-run +``` diff --git a/examples/generate-random-data/example.env b/examples/generate-random-data/example.env new file mode 100644 index 00000000..ce8e0145 --- /dev/null +++ b/examples/generate-random-data/example.env @@ -0,0 +1 @@ +NOTION_KEY=notion-api-key diff --git a/examples/generate-random-data/index.ts b/examples/generate-random-data/index.ts new file mode 100644 index 00000000..098aa12d --- /dev/null +++ b/examples/generate-random-data/index.ts @@ -0,0 +1,402 @@ +// Find the official Notion API client @ https:// github.com/makenotion/notion-sdk-js/ +// npm install @notionhq/client +import { Client } from "@notionhq/client" +import { + CreatePageParameters, + GetDatabaseResponse, + GetPagePropertyResponse, +} from "@notionhq/client/build/src/api-endpoints" + +import * as _ from "lodash" + +import { config } from "dotenv" +config() + +import * as faker from "faker" + +const notion = new Client({ auth: process.env["NOTION_KEY"] }) + +const startTime = new Date() +startTime.setSeconds(0, 0) + +// Given the properties of a database, generate an object full of +// random data that can be used to generate new rows in our Notion database. +function makeFakePropertiesData( + properties: GetDatabaseResponse["properties"] +): Record { + const propertyValues: Record = {} + Object.entries(properties).forEach(([name, property]) => { + if (property.type === "date") { + propertyValues[name] = { + type: "date", + date: { + start: faker.date.past().toISOString(), + }, + } + } else if (property.type === "multi_select") { + const multiSelectOption = _.sample(property.multi_select.options) + if (multiSelectOption) { + propertyValues[name] = { + type: "multi_select", + multi_select: [multiSelectOption], + } + } + } else if (property.type === "select") { + const selectOption = _.sample(property.select.options) + if (selectOption) { + propertyValues[name] = { + type: "select", + id: property.id, + select: selectOption, + } + } + } else if (property.type === "email") { + propertyValues[name] = { + type: "email", + id: property.id, + email: faker.internet.email(), + } + } else if (property.type === "checkbox") { + propertyValues[name] = { + type: "checkbox", + id: property.id, + checkbox: faker.datatype.boolean(), + } + } else if (property.type === "url") { + propertyValues[name] = { + type: "url", + id: property.id, + url: faker.internet.url(), + } + } else if (property.type === "number") { + propertyValues[name] = { + type: "number", + id: property.id, + number: faker.datatype.number(), + } + } else if (property.type === "title") { + propertyValues[name] = { + type: "title", + id: property.id, + title: [ + { + type: "text", + text: { content: faker.lorem.words(3) }, + }, + ], + } + } else if (property.type === "rich_text") { + propertyValues[name] = { + type: "rich_text", + id: property.id, + rich_text: [ + { + type: "text", + text: { content: faker.name.firstName() }, + }, + ], + } + } else if (property.type === "phone_number") { + propertyValues[name] = { + type: "phone_number", + id: property.id, + phone_number: faker.phone.phoneNumber(), + } + } else { + console.log("unimplemented property type: ", property.type) + } + }) + return propertyValues +} + +function assertUnreachable(_x: never): never { + throw new Error("Didn't expect to get here") +} + +function userToString(userBase: { id: string; name?: string | null }) { + return `${userBase.id}: ${userBase.name || "Unknown Name"}` +} + +function findRandomSelectColumnNameAndValue( + properties: GetDatabaseResponse["properties"] +): { + name: string + value: string | undefined +} { + const options = _.flatMap(Object.entries(properties), ([name, property]) => { + if (property.type === "select") { + return [ + { name, value: _.sample(property.select.options.map(o => o.name)) }, + ] + } + return [] + }) + + if (options.length > 0) { + return _.sample(options) || { name: "", value: undefined } + } + + return { name: "", value: undefined } +} + +function extractPropertyItemValueToString( + property: Extract +): string { + switch (property.type) { + case "checkbox": + return property.checkbox.toString() + case "created_by": + return userToString(property.created_by) + case "created_time": + return new Date(property.created_time).toISOString() + case "date": + return property.date ? new Date(property.date.start).toISOString() : "" + case "email": + return property.email ?? "" + case "url": + return property.url ?? "" + case "number": + return typeof property.number === "number" + ? property.number.toString() + : "" + case "phone_number": + return property.phone_number ?? "" + case "select": + if (!property.select) { + return "" + } + return `${property.select.id} ${property.select.name}` + case "multi_select": + if (!property.multi_select) { + return "" + } + return property.multi_select + .map(select => `${select.id} ${select.name}`) + .join(", ") + case "people": + return userToString(property.people) + case "last_edited_by": + return userToString(property.last_edited_by) + case "last_edited_time": + return new Date(property.last_edited_time).toISOString() + case "title": + return property.title.plain_text + case "rich_text": + return property.rich_text.plain_text + case "files": + return property.files.map(file => file.name).join(", ") + case "formula": + if (property.formula.type === "string") { + return property.formula.string || "???" + } else if (property.formula.type === "number") { + return property.formula.number?.toString() || "???" + } else if (property.formula.type === "boolean") { + return property.formula.boolean?.toString() || "???" + } else if (property.formula.type === "date") { + return ( + (property.formula.date?.start && + new Date(property.formula.date.start).toISOString()) || + "???" + ) + } else { + return assertUnreachable(property.formula) + } + case "rollup": + if (property.rollup.type === "number") { + return property.rollup.number?.toString() || "???" + } else if (property.rollup.type === "date") { + return ( + (property.rollup.date?.start && + new Date(property.rollup.date?.start).toISOString()) || + "???" + ) + } else if (property.rollup.type === "array") { + return JSON.stringify(property.rollup.array) + } else if ( + property.rollup.type === "incomplete" || + property.rollup.type === "unsupported" + ) { + return property.rollup.type + } else { + return assertUnreachable(property.rollup) + } + case "relation": + if (property.relation) { + return property.relation.id + } + return "???" + case "status": + return property.status?.name ?? "" + } + return assertUnreachable(property) +} + +function extractValueToString(property: GetPagePropertyResponse): string { + if (property.object === "property_item") { + return extractPropertyItemValueToString(property) + } else if (property.object === "list") { + return property.results + .map(result => extractPropertyItemValueToString(result)) + .join(", ") + } else { + return assertUnreachable(property) + } +} + +async function exerciseWriting( + databaseId: string, + properties: GetDatabaseResponse["properties"] +) { + console.log("\n\n********* Exercising Writing *********\n\n") + + const RowsToWrite = 10 + + // generate a bunch of fake pages with fake data + for (let i = 0; i < RowsToWrite; i++) { + const propertiesData = makeFakePropertiesData(properties) + + const parameters: CreatePageParameters = { + parent: { + database_id: databaseId, + }, + properties: propertiesData, + } as CreatePageParameters + + await notion.pages.create(parameters) + } + + console.log(`Wrote ${RowsToWrite} rows after ${startTime}`) +} + +async function exerciseReading( + databaseId: string, + _properties: GetDatabaseResponse["properties"] +) { + console.log("\n\n********* Exercising Reading *********\n\n") + // and read back what we just did + const queryResponse = await notion.databases.query({ + database_id: databaseId, + }) + let numOldRows = 0 + for (const page of queryResponse.results) { + if (!("url" in page)) { + // Skip partial page objects (these shouldn't be returned anyway.) + continue + } + + const createdTime = new Date(page.created_time) + if (startTime > createdTime) { + numOldRows++ + return + } + + console.log(`New page: ${page.id}`) + + for (const [name, property] of Object.entries(page.properties)) { + const propertyResponse = await notion.pages.properties.retrieve({ + page_id: page.id, + property_id: property.id, + }) + console.log( + ` - ${name} ${property.id} - ${extractValueToString(propertyResponse)}` + ) + } + } + console.log( + `Skipped printing ${numOldRows} rows that were written before ${startTime}` + ) +} + +async function exerciseFilters( + databaseId: string, + properties: GetDatabaseResponse["properties"] +) { + console.log("\n\n********* Exercising Filters *********\n\n") + + // get a random select or multi-select column from the collection with a random value for it + const { name: selectColumnName, value: selectColumnValue } = + findRandomSelectColumnNameAndValue(properties) + + if (!selectColumnName || !selectColumnValue) { + throw new Error("need a select column to run this part of the example") + } + + console.log(`Looking for ${selectColumnName}=${selectColumnValue}`) + + // Check we can search by name + const queryFilterSelectFilterTypeBased = { + property: selectColumnName, + select: { equals: selectColumnValue }, + } + + const matchingSelectResults = await notion.databases.query({ + database_id: databaseId, + filter: queryFilterSelectFilterTypeBased, + }) + + console.log( + `had ${matchingSelectResults.results.length} matching rows for ${selectColumnName}=${selectColumnValue}` + ) + + // Let's do it again for text + + const textColumn = _.sample( + Object.values(properties).filter(p => p.type === "rich_text") + ) + if (!textColumn) { + throw new Error( + "Need a rich_text column for this part of the test, could not find one" + ) + } + const textColumnId = decodeURIComponent(textColumn.id) + const letterToFind = faker.lorem.word(1) + + console.log( + `\n\nLooking for text column with id "${textColumnId}" contains letter "${letterToFind}"` + ) + + const textFilter = { + property: textColumnId, + rich_text: { contains: letterToFind }, + } + + // Check we can search by id + const matchingTextResults = await notion.databases.query({ + database_id: databaseId, + filter: textFilter, + }) + + console.log( + `Had ${matchingTextResults.results.length} matching rows in column with ID "${textColumnId}" containing letter "${letterToFind}"` + ) +} + +async function main() { + // Find the first database this bot has access to + const databases = await notion.search({ + filter: { + property: "object", + value: "database", + }, + }) + + if (databases.results.length === 0) { + throw new Error("This bot doesn't have access to any databases!") + } + + const database = databases.results[0] + if (!database) { + throw new Error("This bot doesn't have access to any databases!") + } + + // Get the database properties out of our database + const { properties } = await notion.databases.retrieve({ + database_id: database.id, + }) + + await exerciseWriting(database.id, properties) + await exerciseReading(database.id, properties) + await exerciseFilters(database.id, properties) +} + +main() diff --git a/examples/generate-random-data/package.json b/examples/generate-random-data/package.json new file mode 100644 index 00000000..b6de4818 --- /dev/null +++ b/examples/generate-random-data/package.json @@ -0,0 +1,20 @@ +{ + "name": "generate-random-data", + "version": "1.0.1", + "description": "Generate random data in a Notion Database.", + "main": "index.js", + "scripts": { + "ts-run": "node --loader ts-node/esm index.ts" + }, + "author": "David Blackman", + "license": "MIT", + "dependencies": { + "@notionhq/client": "file:../../", + "@types/faker": "^5.5.5", + "@types/lodash": "^4.14.170", + "dotenv": "^8.2.0", + "faker": "^5.5.3", + "lodash": "^4.17.21", + "ts-node": "^10.8.2" + } +} diff --git a/examples/intro-to-notion-api/.env.example b/examples/intro-to-notion-api/.env.example new file mode 100644 index 00000000..b8f71102 --- /dev/null +++ b/examples/intro-to-notion-api/.env.example @@ -0,0 +1,2 @@ +NOTION_API_KEY= +NOTION_PAGE_ID= \ No newline at end of file diff --git a/examples/intro-to-notion-api/README.md b/examples/intro-to-notion-api/README.md new file mode 100644 index 00000000..8409c6df --- /dev/null +++ b/examples/intro-to-notion-api/README.md @@ -0,0 +1,97 @@ +# Introduction to using Notion's SDK for JavaScript + +## Learn how to make Public API requests + +Use this sample code to learn how to make Public API requests with varying degrees of difficulty. + +The sample code is split into two sections: + +1. `basic` +2. `intermediate` + +(An `advanced` section will soon be added, as well.) + +If you are new to Notion's SDK for JavaScript, start with the code samples in the `/basic` directory to get more familiar to basic concepts. + +The files in each directory will build on each other to increase in complexity. For example, in `/intermediate`, first you will see how to create a database, then how to create a database and add a page to it, and finally create a database, add a page, and query/sort the database. + +## Table of contents + +In case you are looking for example code for a specific task, the files are divided as follows: + +- `/basic/1-add-block.js`: Create a new block and append it to an existing Notion page. +- `/basic/2-add-linked-block.js`: Create and append new blocks, and add a link to the text of a new block. +- `/basic/3-add-styled-block.js`: Create and append new blocks, and apply text styles to them. +- `/intermediate/1-create-a-database.js`: Create a new database with defined properties. +- `/intermediate/2-add-page-to-database.js`: Create a new database and add new pages to it. +- `/intermediate/3-query-database.js`: Create a new database, add pages to it, and filter the database entries (pages). +- `/intermediate/4-sort-database.js`: Create a new database, add pages to it, and filter/sort the database entries (pages). + +## Running locally + +### 1. Clone project and install dependencies + +To use this example on your machine, clone the repo and move into your local copy: + +```zsh +git clone https://github.com/makenotion/notion-sdk-js.git +cd /notion-sdk-js +``` + +Next, move into this example in the `/examples` directory, and install its dependencies: + +```zsh +cd /examples/intro-to-notion-api +npm install +``` + +### 2. Set your environment variables in a `.env` file + +A `.env.example` file has been included and can be renamed `.env` (or you can run `cp .env.example .env` to copy the file). + +Update the environment variables below: + +```zsh +NOTION_API_KEY= +NOTION_PAGE_ID= +``` + +`NOTION_API_KEY`: Create a new integration in the [integrations dashboard](https://www.notion.com/my-integrations) and retrieve the API key from the integration's `Secrets` page. + +`NOTION_PAGE_ID`: Use the ID of any Notion page that you want to test adding content to. + +The page ID is the 32 character string at the end of any page URL. +![A Notion page URL with the ID highlighted](./assets/page_id.png) + +### 3. Give the integration access to your page + +Your Notion integration will need permission to interact with the Notion page being used for your `NOTION_PAGE_ID` variable. To provide access, do the following: + +1. Go to the page in your workspace. +2. Click the `•••` (more menu) on the top-right corner of the page. +3. Scroll to the bottom of the menu and click `Add connections`. +4. Search for and select your integration in the `Search for connections...` menu. + +Once selected, your integration will have permission to read content from the page. + +**If you are receiving authorization errors, make sure the integration has permission to access the page.** + +### 3. Run individual examples + +To run each individual example, use the `node` command with the file's path. + +For example: + +```zsh +node /basic/1-add-block.js +``` + +--- + +## Additional resources + +To learn more, read the [Public API docs](https://developers.notion.com/) for additional information on using Notion's API. The API docs include a series of [guides](https://developers.notion.com/docs) and the [API reference](https://developers.notion.com/reference/intro), which has a full list of available endpoints. + +To see more examples of what you can build with Notion, see our other sample integrations in the parent `/examples` directory. To learn how to build an internal integration with an interactive frontend, read the [Build your first integration](https://developers.notion.com/docs/create-a-notion-integration) guide. + +To connect with other developers building with Notion, join the [Notion Developers Slack group](https://join.slack.com/t/notiondevs/shared_invite/zt-20b5996xv-DzJdLiympy6jP0GGzu3AMg). diff --git a/examples/intro-to-notion-api/assets/page_id.png b/examples/intro-to-notion-api/assets/page_id.png new file mode 100644 index 00000000..d0e837b6 Binary files /dev/null and b/examples/intro-to-notion-api/assets/page_id.png differ diff --git a/examples/intro-to-notion-api/basic/1-add-block.js b/examples/intro-to-notion-api/basic/1-add-block.js new file mode 100644 index 00000000..a245215d --- /dev/null +++ b/examples/intro-to-notion-api/basic/1-add-block.js @@ -0,0 +1,45 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Appending block children endpoint (notion.blocks.children.append(): https://developers.notion.com/reference/patch-block-children) + * - Working with page content guide: https://developers.notion.com/docs/working-with-page-content + */ + +async function main() { + const blockId = pageId // Blocks can be appended to other blocks *or* pages. Therefore, a page ID can be used for the block_id parameter + const newHeadingResponse = await notion.blocks.children.append({ + block_id: blockId, + // Pass an array of blocks to append to the page: https://developers.notion.com/reference/block#block-type-objects + children: [ + { + heading_2: { + rich_text: [ + { + text: { + content: "Types of kale", // This is the text that will be displayed in Notion + }, + }, + ], + }, + }, + ], + }) + + // Print the new block(s) response + console.log(newHeadingResponse) +} + +main() diff --git a/examples/intro-to-notion-api/basic/2-add-linked-block.js b/examples/intro-to-notion-api/basic/2-add-linked-block.js new file mode 100644 index 00000000..54424b23 --- /dev/null +++ b/examples/intro-to-notion-api/basic/2-add-linked-block.js @@ -0,0 +1,62 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Appending block children endpoint (notion.blocks.children.append(): https://developers.notion.com/reference/patch-block-children) + * - Rich text options: https://developers.notion.com/reference/rich-text + * - Working with page content guide: https://developers.notion.com/docs/working-with-page-content + */ + +async function main() { + const blockId = pageId // Blocks can be appended to other blocks *or* pages. Therefore, a page ID can be used for the block_id parameter + const linkedTextResponse = await notion.blocks.children.append({ + block_id: blockId, + // Pass an array of blocks to append to the page: https://developers.notion.com/reference/block#block-type-objects + children: [ + { + heading_3: { + rich_text: [ + { + text: { + content: "Tuscan kale", // This is the text that will be displayed in Notion + }, + }, + ], + }, + }, + { + paragraph: { + rich_text: [ + { + text: { + content: + "Tuscan kale is a variety of kale with a long tradition in Italian cuisine, especially that of Tuscany. It is also known as Tuscan kale, Italian kale, dinosaur kale, kale, flat back kale, palm tree kale, or black Tuscan palm.", + link: { + // Include a url to make the paragraph a link in Notion + url: "https://en.wikipedia.org/wiki/Kale", + }, + }, + }, + ], + }, + }, + ], + }) + + // Print the new block(s) response + console.log(linkedTextResponse) +} + +main() diff --git a/examples/intro-to-notion-api/basic/3-add-styled-block.js b/examples/intro-to-notion-api/basic/3-add-styled-block.js new file mode 100644 index 00000000..7919638e --- /dev/null +++ b/examples/intro-to-notion-api/basic/3-add-styled-block.js @@ -0,0 +1,70 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Appending block children endpoint (notion.blocks.children.append(): https://developers.notion.com/reference/patch-block-children) + * - Rich text options: https://developers.notion.com/reference/rich-text + * - Working with page content guide: https://developers.notion.com/docs/working-with-page-content + */ + +async function main() { + const blockId = pageId // Blocks can be appended to other blocks *or* pages. Therefore, a page ID can be used for the block_id parameter + const styledLinkTextResponse = await notion.blocks.children.append({ + block_id: blockId, + children: [ + { + heading_3: { + rich_text: [ + { + text: { + content: "Tuscan kale", + }, + }, + ], + }, + }, + { + paragraph: { + rich_text: [ + { + text: { + // Paragraph text + content: + "Tuscan kale is a variety of kale with a long tradition in Italian cuisine, especially that of Tuscany. It is also known as Tuscan kale, Italian kale, dinosaur kale, kale, flat back kale, palm tree kale, or black Tuscan palm.", + link: { + // Paragraph link + url: "https://en.wikipedia.org/wiki/Kale", + }, + }, + annotations: { + // Paragraph styles + bold: true, + italic: true, + strikethrough: true, + underline: true, + color: "green", + }, + }, + ], + }, + }, + ], + }) + + // Print the new block(s) response + console.log(styledLinkTextResponse) +} + +main() diff --git a/examples/intro-to-notion-api/intermediate/1-create-a-database.js b/examples/intro-to-notion-api/intermediate/1-create-a-database.js new file mode 100644 index 00000000..589a403b --- /dev/null +++ b/examples/intro-to-notion-api/intermediate/1-create-a-database.js @@ -0,0 +1,59 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Create a database endpoint (notion.databases.create(): https://developers.notion.com/reference/create-a-database) + * - Working with databases guide: https://developers.notion.com/docs/working-with-databases + */ + +async function main() { + // Create a new database + const newDatabase = await notion.databases.create({ + parent: { + type: "page_id", + page_id: pageId, + }, + title: [ + { + type: "text", + text: { + content: "New database name", + }, + }, + ], + properties: { + // These properties represent columns in the database (i.e. its schema) + "Grocery item": { + type: "title", + title: {}, + }, + Price: { + type: "number", + number: { + format: "dollar", + }, + }, + "Last ordered": { + type: "date", + date: {}, + }, + }, + }) + + // Print the new database response + console.log(newDatabase) +} + +main() diff --git a/examples/intro-to-notion-api/intermediate/2-add-page-to-database.js b/examples/intro-to-notion-api/intermediate/2-add-page-to-database.js new file mode 100644 index 00000000..5d1ab132 --- /dev/null +++ b/examples/intro-to-notion-api/intermediate/2-add-page-to-database.js @@ -0,0 +1,81 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" +import { propertiesForNewPages } from "./sampleData.js" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Create a database endpoint (notion.databases.create(): https://developers.notion.com/reference/create-a-database) + * - Create a page endpoint (notion.pages.create(): https://developers.notion.com/reference/post-page) + * - Working with databases guide: https://developers.notion.com/docs/working-with-databases + */ + +async function addNotionPageToDatabase(databaseId, pageProperties) { + const newPage = await notion.pages.create({ + parent: { + database_id: databaseId, + }, + properties: pageProperties, + }) + console.log(newPage) +} + +async function main() { + // Create a new database + const newDatabase = await notion.databases.create({ + parent: { + type: "page_id", + page_id: pageId, + }, + title: [ + { + type: "text", + text: { + content: "Grocery list", + }, + }, + ], + properties: { + // These properties represent columns in the database (i.e. its schema) + "Grocery item": { + type: "title", + title: {}, + }, + Price: { + type: "number", + number: { + format: "dollar", + }, + }, + "Last ordered": { + type: "date", + date: {}, + }, + }, + }) + + // Print the new database's URL. Visit the URL in your browser to see the pages that get created in the next step. + console.log(newDatabase.url) + + const databaseId = newDatabase.id + // If there is no ID (if there's an error), return. + if (!databaseId) return + + console.log("Adding new pages...") + for (let i = 0; i < propertiesForNewPages.length; i++) { + // Add a few new pages to the database that was just created + await addNotionPageToDatabase(databaseId, propertiesForNewPages[i]) + } +} + +main() diff --git a/examples/intro-to-notion-api/intermediate/3-query-database.js b/examples/intro-to-notion-api/intermediate/3-query-database.js new file mode 100644 index 00000000..535d3716 --- /dev/null +++ b/examples/intro-to-notion-api/intermediate/3-query-database.js @@ -0,0 +1,102 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" +import { propertiesForNewPages } from "./sampleData.js" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Create a database endpoint (notion.databases.create(): https://developers.notion.com/reference/create-a-database) + * - Create a page endpoint (notion.pages.create(): https://developers.notion.com/reference/post-page) + * - Working with databases guide: https://developers.notion.com/docs/working-with-databases + * Query a database: https://developers.notion.com/reference/post-database-query + * Filter database entries: https://developers.notion.com/reference/post-database-query-filter + */ + +async function addNotionPageToDatabase(databaseId, pageProperties) { + await notion.pages.create({ + parent: { + database_id: databaseId, + }, + properties: pageProperties, // Note: Page properties must match the schema of the database + }) +} + +async function queryDatabase(databaseId) { + console.log("Querying database...") + // This query will filter database entries and return pages that have a "Last ordered" property that is more recent than 2022-12-31. Use multiple filters with the AND/OR options: https://developers.notion.com/reference/post-database-query-filter. + const lastOrderedIn2023 = await notion.databases.query({ + database_id: databaseId, + filter: { + property: "Last ordered", + date: { + after: "2022-12-31", + }, + }, + }) + + // Print filtered results + console.log('Pages with the "Last ordered" date after 2022-12-31:') + console.log(lastOrderedIn2023) +} + +async function main() { + // Create a new database + const newDatabase = await notion.databases.create({ + parent: { + type: "page_id", + page_id: pageId, + }, + title: [ + { + type: "text", + text: { + content: "Grocery list", + }, + }, + ], + properties: { + // These properties represent columns in the database (i.e. its schema) + "Grocery item": { + type: "title", + title: {}, + }, + Price: { + type: "number", + number: { + format: "dollar", + }, + }, + "Last ordered": { + type: "date", + date: {}, + }, + }, + }) + // Print the new database's URL. Visit the URL in your browser to see the pages that get created in the next step. + console.log(newDatabase.url) + + const databaseId = newDatabase.id + // If there is no ID (if there's an error), return. + if (!databaseId) return + + console.log("Adding new pages...") + for (let i = 0; i < propertiesForNewPages.length; i++) { + // Add a few new pages to the database that was just created + await addNotionPageToDatabase(databaseId, propertiesForNewPages[i]) + } + + // After adding pages, query the database entries (pages) + queryDatabase(databaseId) +} + +main() diff --git a/examples/intro-to-notion-api/intermediate/4-sort-database.js b/examples/intro-to-notion-api/intermediate/4-sort-database.js new file mode 100644 index 00000000..36b3ffe4 --- /dev/null +++ b/examples/intro-to-notion-api/intermediate/4-sort-database.js @@ -0,0 +1,110 @@ +import { Client } from "@notionhq/client" +import { config } from "dotenv" +import { propertiesForNewPages } from "./sampleData.js" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +/** + * Resources: + * - Create a database endpoint (notion.databases.create(): https://developers.notion.com/reference/create-a-database) + * - Create a page endpoint (notion.pages.create(): https://developers.notion.com/reference/post-page) + * - Working with databases guide: https://developers.notion.com/docs/working-with-databases + * Query a database: https://developers.notion.com/reference/post-database-query + * Filter database entries: https://developers.notion.com/reference/post-database-query-filter + * Sort database entries: https://developers.notion.com/reference/post-database-query-sort + */ + +async function addNotionPageToDatabase(databaseId, pageProperties) { + await notion.pages.create({ + parent: { + database_id: databaseId, + }, + properties: pageProperties, // Note: Page properties must match the schema of the database + }) +} + +async function queryAndSortDatabase(databaseId) { + console.log("Querying database...") + // This query will filter and sort database entries. The returned pages will have a "Last ordered" property that is more recent than 2022-12-31. Any database property can be filtered or sorted. Pass multiple sort objects to the "sorts" array to apply more than one sorting rule. + const lastOrderedIn2023Alphabetical = await notion.databases.query({ + database_id: databaseId, + filter: { + property: "Last ordered", + date: { + after: "2022-12-31", + }, + }, + sorts: [ + { + property: "Grocery item", + direction: "descending", + }, + ], + }) + + // Print filtered/sorted results + console.log( + 'Pages with the "Last ordered" date after 2022-12-31 in descending order:' + ) + console.log(lastOrderedIn2023Alphabetical) +} + +async function main() { + // Create a new database + const newDatabase = await notion.databases.create({ + parent: { + type: "page_id", + page_id: pageId, + }, + title: [ + { + type: "text", + text: { + content: "Grocery list", + }, + }, + ], + properties: { + // These properties represent columns in the database (i.e. its schema) + "Grocery item": { + type: "title", + title: {}, + }, + Price: { + type: "number", + number: { + format: "dollar", + }, + }, + "Last ordered": { + type: "date", + date: {}, + }, + }, + }) + // Print the new database's URL. Visit the URL in your browser to see the pages that get created in the next step. + console.log(newDatabase.url) + + const databaseId = newDatabase.id + if (!databaseId) return + + console.log("Adding new pages...") + for (let i = 0; i < propertiesForNewPages.length; i++) { + // Add a few new pages to the database that was just created + await addNotionPageToDatabase(databaseId, propertiesForNewPages[i]) + } + + // After adding pages, query the database entries (pages) and sort the results + queryAndSortDatabase(databaseId) +} + +main() diff --git a/examples/intro-to-notion-api/intermediate/sampleData.js b/examples/intro-to-notion-api/intermediate/sampleData.js new file mode 100644 index 00000000..4d03353c --- /dev/null +++ b/examples/intro-to-notion-api/intermediate/sampleData.js @@ -0,0 +1,44 @@ +export const propertiesForNewPages = [ + { + "Grocery item": { + type: "title", + title: [{ type: "text", text: { content: "Tomatoes" } }], + }, + Price: { + type: "number", + number: 1.49, + }, + "Last ordered": { + type: "date", + date: { start: "2023-05-11" }, + }, + }, + { + "Grocery item": { + type: "title", + title: [{ type: "text", text: { content: "Lettuce" } }], + }, + Price: { + type: "number", + number: 3.99, + }, + "Last ordered": { + type: "date", + date: { start: "2023-05-04" }, + }, + }, + { + "Grocery item": { + type: "title", + title: [{ type: "text", text: { content: "Oranges" } }], + }, + Price: { + type: "number", + number: 0.99, + }, + "Last ordered": { + type: "date", + date: { start: "2022-04-29" }, + }, + }, +] diff --git a/examples/intro-to-notion-api/package.json b/examples/intro-to-notion-api/package.json new file mode 100644 index 00000000..ad005d27 --- /dev/null +++ b/examples/intro-to-notion-api/package.json @@ -0,0 +1,24 @@ +{ + "name": "intro-to-notion-api", + "version": "1.0.0", + "description": "Introductory examples of using Notion's public API via the Notion SDK for JavaScript.", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/makenotion/notion-sdk-js.git" + }, + "author": "Jess Mitchell", + "license": "MIT", + "bugs": { + "url": "https://github.com/makenotion/notion-sdk-js/issues" + }, + "dependencies": { + "@notionhq/client": "file:../..", + "dotenv": "^16.3.1" + } +} diff --git a/examples/notion-github-sync/.prettierrc b/examples/notion-github-sync/.prettierrc new file mode 100644 index 00000000..302f65ea --- /dev/null +++ b/examples/notion-github-sync/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "avoid", + "tabWidth": 2, + "semi": false, + "trailingComma": "es5", + "endOfLine": "lf", + "singleQuote": false +} diff --git a/examples/notion-github-sync/README.md b/examples/notion-github-sync/README.md new file mode 100644 index 00000000..8472c02a --- /dev/null +++ b/examples/notion-github-sync/README.md @@ -0,0 +1,46 @@ +# Sample Integration: GitHub Issues to Notion + +drawing + +## About the Integration + +This Notion integration syncs GitHub Issues for a specific repo to a Notion Database. This integration was built using this [database template](https://www.notion.com/367cd67cfe8f49bfaf0ac21305ebb9bf?v=bc79ca62b36e4c54b655ceed4ef06ebd) and [GitHub's Octokit Library](https://github.com/octokit). Changes made to issues in the Notion database will not be reflected in GitHub. For an example which allows you to take actions based on changes in a database [go here.](https://github.com/makenotion/notion-sdk-js/tree/main/examples/database-email-update) + +## Running Locally + +### 1. Setup your local project + +```zsh +# Clone this repository locally +git clone https://github.com/makenotion/notion-sdk-js.git + +# Switch into this project +cd notion-sdk-js/examples/notion-github-sync + +# Install the dependencies +npm install +``` + +### 2. Set your environment variables in a `.env` file + +```zsh +GITHUB_KEY= +NOTION_KEY= +NOTION_DATABASE_ID= +GITHUB_REPO_OWNER= +GITHUB_REPO_NAME= +``` + +You can create your Notion API key [here](https://www.notion.com/my-integrations). + +You can create your GitHub Personal Access token by following the guide [here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). + +To create a Notion database that will work with this example, duplicate [this empty database template](https://www.notion.com/367cd67cfe8f49bfaf0ac21305ebb9bf?v=bc79ca62b36e4c54b655ceed4ef06ebd). + +Your Notion integration will need access to the Notion database you have created. To provide access, follow the instructions found in Notion's [Integration guide](https://developers.notion.com/docs/create-a-notion-integration#step-2-share-a-database-with-your-integration). + +### 3. Run code + +```zsh +node index.js +``` diff --git a/examples/notion-github-sync/index.js b/examples/notion-github-sync/index.js new file mode 100644 index 00000000..a7b3778b --- /dev/null +++ b/examples/notion-github-sync/index.js @@ -0,0 +1,232 @@ +/* ================================================================================ + + notion-github-sync. + + Glitch example: https://glitch.com/edit/#!/notion-github-sync + Find the official Notion API client @ https://github.com/makenotion/notion-sdk-js/ + +================================================================================ */ + +const { Client } = require("@notionhq/client") +const dotenv = require("dotenv") +const { Octokit } = require("octokit") +const _ = require("lodash") + +dotenv.config() +const octokit = new Octokit({ auth: process.env.GITHUB_KEY }) +const notion = new Client({ auth: process.env.NOTION_KEY }) + +const databaseId = process.env.NOTION_DATABASE_ID +const OPERATION_BATCH_SIZE = 10 + +/** + * Local map to store GitHub issue ID to its Notion pageId. + * { [issueId: string]: string } + */ +const gitHubIssuesIdToNotionPageId = {} + +/** + * Initialize local data store. + * Then sync with GitHub. + */ +setInitialGitHubToNotionIdMap().then(syncNotionDatabaseWithGitHub) + +/** + * Get and set the initial data store with issues currently in the database. + */ +async function setInitialGitHubToNotionIdMap() { + const currentIssues = await getIssuesFromNotionDatabase() + for (const { pageId, issueNumber } of currentIssues) { + gitHubIssuesIdToNotionPageId[issueNumber] = pageId + } +} + +async function syncNotionDatabaseWithGitHub() { + // Get all issues currently in the provided GitHub repository. + console.log("\nFetching issues from GitHub repository...") + const issues = await getGitHubIssuesForRepository() + console.log(`Fetched ${issues.length} issues from GitHub repository.`) + + // Group issues into those that need to be created or updated in the Notion database. + const { pagesToCreate, pagesToUpdate } = getNotionOperations(issues) + + // Create pages for new issues. + console.log(`\n${pagesToCreate.length} new issues to add to Notion.`) + await createPages(pagesToCreate) + + // Updates pages for existing issues. + console.log(`\n${pagesToUpdate.length} issues to update in Notion.`) + await updatePages(pagesToUpdate) + + // Success! + console.log("\n✅ Notion database is synced with GitHub.") +} + +/** + * Gets pages from the Notion database. + * + * @returns {Promise>} + */ +async function getIssuesFromNotionDatabase() { + const pages = [] + let cursor = undefined + while (true) { + const { results, next_cursor } = await notion.databases.query({ + database_id: databaseId, + start_cursor: cursor, + }) + pages.push(...results) + if (!next_cursor) { + break + } + cursor = next_cursor + } + console.log(`${pages.length} issues successfully fetched.`) + + const issues = [] + for (const page of pages) { + const issueNumberPropertyId = page.properties["Issue Number"].id + const propertyResult = await notion.pages.properties.retrieve({ + page_id: page.id, + property_id: issueNumberPropertyId, + }) + issues.push({ + pageId: page.id, + issueNumber: propertyResult.number, + }) + } + + return issues +} + +/** + * Gets issues from a GitHub repository. Pull requests are omitted. + * + * https://docs.github.com/en/rest/guides/traversing-with-pagination + * https://docs.github.com/en/rest/reference/issues + * + * @returns {Promise>} + */ +async function getGitHubIssuesForRepository() { + const issues = [] + const iterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, { + owner: process.env.GITHUB_REPO_OWNER, + repo: process.env.GITHUB_REPO_NAME, + state: "all", + per_page: 100, + }) + for await (const { data } of iterator) { + for (const issue of data) { + if (!issue.pull_request) { + issues.push({ + number: issue.number, + title: issue.title, + state: issue.state, + comment_count: issue.comments, + url: issue.html_url, + }) + } + } + } + return issues +} + +/** + * Determines which issues already exist in the Notion database. + * + * @param {Array<{ number: number, title: string, state: "open" | "closed", comment_count: number, url: string }>} issues + * @returns {{ + * pagesToCreate: Array<{ number: number, title: string, state: "open" | "closed", comment_count: number, url: string }>; + * pagesToUpdate: Array<{ pageId: string, number: number, title: string, state: "open" | "closed", comment_count: number, url: string }> + * }} + */ +function getNotionOperations(issues) { + const pagesToCreate = [] + const pagesToUpdate = [] + for (const issue of issues) { + const pageId = gitHubIssuesIdToNotionPageId[issue.number] + if (pageId) { + pagesToUpdate.push({ + ...issue, + pageId, + }) + } else { + pagesToCreate.push(issue) + } + } + return { pagesToCreate, pagesToUpdate } +} + +/** + * Creates new pages in Notion. + * + * https://developers.notion.com/reference/post-page + * + * @param {Array<{ number: number, title: string, state: "open" | "closed", comment_count: number, url: string }>} pagesToCreate + */ +async function createPages(pagesToCreate) { + const pagesToCreateChunks = _.chunk(pagesToCreate, OPERATION_BATCH_SIZE) + for (const pagesToCreateBatch of pagesToCreateChunks) { + await Promise.all( + pagesToCreateBatch.map(issue => + notion.pages.create({ + parent: { database_id: databaseId }, + properties: getPropertiesFromIssue(issue), + }) + ) + ) + console.log(`Completed batch size: ${pagesToCreateBatch.length}`) + } +} + +/** + * Updates provided pages in Notion. + * + * https://developers.notion.com/reference/patch-page + * + * @param {Array<{ pageId: string, number: number, title: string, state: "open" | "closed", comment_count: number, url: string }>} pagesToUpdate + */ +async function updatePages(pagesToUpdate) { + const pagesToUpdateChunks = _.chunk(pagesToUpdate, OPERATION_BATCH_SIZE) + for (const pagesToUpdateBatch of pagesToUpdateChunks) { + await Promise.all( + pagesToUpdateBatch.map(({ pageId, ...issue }) => + notion.pages.update({ + page_id: pageId, + properties: getPropertiesFromIssue(issue), + }) + ) + ) + console.log(`Completed batch size: ${pagesToUpdateBatch.length}`) + } +} + +//*======================================================================== +// Helpers +//*======================================================================== + +/** + * Returns the GitHub issue to conform to this database's schema properties. + * + * @param {{ number: number, title: string, state: "open" | "closed", comment_count: number, url: string }} issue + */ +function getPropertiesFromIssue(issue) { + const { title, number, state, comment_count, url } = issue + return { + Name: { + title: [{ type: "text", text: { content: title } }], + }, + "Issue Number": { + number, + }, + State: { + select: { name: state }, + }, + "Number of Comments": { + number: comment_count, + }, + "Issue URL": { + url, + }, + } +} diff --git a/examples/notion-github-sync/package.json b/examples/notion-github-sync/package.json new file mode 100644 index 00000000..2a91d627 --- /dev/null +++ b/examples/notion-github-sync/package.json @@ -0,0 +1,21 @@ +{ + "name": "github-issue-sync", + "version": "1.0.0", + "description": "Sync GitHub issues with Notion.", + "main": "index.js", + "engines": { + "node": "12.x" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "" + }, + "author": "Aman Gupta", + "license": "MIT", + "dependencies": { + "@notionhq/client": "file:../../", + "dotenv": "^16.0.1", + "lodash": "^4.17.21", + "octokit": "^2.0.3" + } +} diff --git a/examples/notion-task-github-pr-sync/README.md b/examples/notion-task-github-pr-sync/README.md new file mode 100644 index 00000000..fffb26b1 --- /dev/null +++ b/examples/notion-task-github-pr-sync/README.md @@ -0,0 +1,91 @@ +# Sample Integration: GitHub PRs to Notion + +drawing + +## About the Integration + +This Notion integration updates Notion tasks when a linked Github PR is closed/merged. This integration requires the Notion task link be mentioned in the PR description. This example will guide you through setting up a Notion database, creating a Notion integration and sharing the database with the integration. + +## Running Locally + +### 1. Setup your local project + +```zsh +# Clone this repository locally +git clone https://github.com/makenotion/notion-sdk-js.git + +# Switch into this project +cd notion-sdk-js/examples/notion-task-github-pr-sync + +# Install the dependencies +npm install +``` + +### 2. Create Github Personal Access Token + +In order for this Integration to work with Github, you'll need a Github Personal Access Token. You can create your GitHub Personal Access token by following the guide [here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). Make a note of the Personal Access token, we'll need it later. + +### 3. Setup Github Repository + +If you don't have a Github Repository you may [click here](https://github.com/new) to set one up. + +Once you've created a repository or if you already have one, make a note of the repository owner and repository name, we'll need it later. + +### 4. Create a Notion Database + +You may skip this step if you have a Notion database to use with this integration. + +If you don't have a Notion database, you may duplicate [this empty database template](https://www.notion.so/Example-Notion-Tasks-Database-93cf694c6b8c4a829ef3fb389ac62d4e), to get started. + +If you'd like this Integration to also update a Database Status property, you should create one now. + +If you already have one, make note of the Status Property Name, we'll need it later. + +You may choose whether or not to update the Status Property by setting + +``` +UPDATE_STATUS_IN_NOTION_DB = +``` + +in your .env file to true or false. More on this later. + +### 5. Create Notion Integration + +In order to leverage the Notion API, we must first create an integration. To do that, [click here](https://www.notion.com/my-integrations), and click Create new integration. + +As you progress through the fields, pay close attention to enabling the following permissions: + +Capabilities > Content Capabilities > Read Content, Update Content, Insert Content. +Capabilities > Comment Capabilities > Read Comments, Insert Comments + +These capabilities are required to write comments on your Notion Tasks. + +Once your integration is created, make a note of your Internal Integration Token, this will be your Notion API Key, we'll need it in the next step. + +drawing + +### 6. Connect your Integration with your Notion Page + +1. Go to the database page in your workspace. +2. Click the ••• on the top right corner of the page. +3. At the bottom of the pop-up, click Add connections. +4. Search for and select your integration in the Search for connections... menu. + +### 7. Set your environment variables in a `.env` file + +Using the information you noted above, create a `.env` file. + +``` +GITHUB_KEY= +NOTION_KEY= +GITHUB_REPO_OWNER= +GITHUB_REPO_NAME= +UPDATE_STATUS_IN_NOTION_DB = +STATUS_PROPERTY_NAME = +``` + +### 8. Run code + +```zsh +node index.js +``` diff --git a/examples/notion-task-github-pr-sync/index.js b/examples/notion-task-github-pr-sync/index.js new file mode 100644 index 00000000..613eb77a --- /dev/null +++ b/examples/notion-task-github-pr-sync/index.js @@ -0,0 +1,189 @@ +/* ================================================================================ + + notion-task-github-pr-sync + + Find the official Notion API client @ https://github.com/makenotion/notion-sdk-js/ + Glitch Example: https://glitch.com/~notion-task-github-pr-sync + +================================================================================ */ + +const { Client } = require("@notionhq/client") +const dotenv = require("dotenv") +const { Octokit } = require("octokit") +const _ = require("lodash") + +dotenv.config() +const octokit = new Octokit({ auth: process.env.GITHUB_KEY }) +const notion = new Client({ auth: process.env.NOTION_KEY }) + +const OPERATION_BATCH_SIZE = 10 + +/** + * Enable to change status property in Notion Database + * When enabling this, make sure you have a set the STATUS_FIELD_NAME + */ +const UPDATE_STATUS_IN_NOTION_DB = process.env.UPDATE_STATUS_IN_NOTION_DB +const STATUS_PROPERTY_NAME = process.env.STATUS_PROPERTY_NAME + +/** + * Entry Point + */ +updateNotionDBwithGithubPRs() + +/** + * Fetches PRs from Github and updates the according Notion Task + */ +async function updateNotionDBwithGithubPRs() { + // Get all issues currently in the provided GitHub repository. + console.log("\nFetching PRs from GitHub repository...") + var prs = await getGitHubPRsForRepository() + console.log(`Fetched ${prs.length} closed PR(s) from GitHub repository.`) + + var prsToUpdate = [] + for (var pr of prs) { + if (!(await hasIntegrationCommentedOnPage(pr.page_id))) { + prsToUpdate.push(pr) + } + } + updatePages(prsToUpdate) +} + +/** + * Returns whether integration has commented + * @params page_id: string + * @returns {Promise} + */ +async function hasIntegrationCommentedOnPage(page_id) { + const comments = await notion.comments.list({ block_id: page_id }) + const bot = await notion.users.me() + if (comments.results) { + for (const comment of comments.results) { + if (comment.created_by.id === bot.id) { + return true + } + } + } + return false +} + +/** + * Gets closed PRs from a GitHub repository. + * + * https://docs.github.com/en/rest/guides/traversing-with-pagination + * https://docs.github.com/en/rest/pulls/pulls#list-pull-requests + * https://octokit.github.io/rest.js/v19#pulls-list + * + * @returns {Promise>} + */ +async function getGitHubPRsForRepository() { + const pullRequests = [] + const iterator = octokit.paginate.iterator(octokit.rest.pulls.list, { + owner: process.env.GITHUB_REPO_OWNER, + repo: process.env.GITHUB_REPO_NAME, + state: "all", + per_page: 100, + }) + for await (const { data } of iterator) { + for (const pr of data) { + if (pr.body) { + notionPRLinkMatch = pr.body.match( + /https:\/\/www\.notion\.so\/([A-Za-z0-9]+(-[A-Za-z0-9]+)+)$/ + ) + if (notionPRLinkMatch && pr.state == "closed") { + const page_id = notionPRLinkMatch[0] + .split("-") + .pop() + .replaceAll("-", "") + + var status = "" + var content = "" + if (pr.merged_at != null) { + status = "Closed - Merged" + content = " has been merged!" + } else { + status = "Closed - Not Merged" + content = " was closed but not merged!" + } + + pullRequests.push({ + task_link: notionPRLinkMatch[0], + state: pr.state, + page_id: page_id, + pr_link: pr.html_url, + pr_status: status, + comment_content: content, + }) + } + } else { + console.log("Error: PR body is empty") + } + } + return pullRequests + } +} + +/*** + * + * @param pagesToUpdate: [pages] + * @returns Promise + */ +async function updatePages(pagesToUpdate) { + const pagesToUpdateChunks = _.chunk(pagesToUpdate, OPERATION_BATCH_SIZE) + for (const pagesToUpdateBatch of pagesToUpdateChunks) { + //Update page status property + if (UPDATE_STATUS_IN_NOTION_DB) { + await Promise.all( + pagesToUpdateBatch.map(({ ...pr }) => + //Update Notion Page status + notion.pages.update({ + page_id: pr.page_id, + properties: { + [STATUS_PROPERTY_NAME]: { + status: { + name: pr.pr_status, + }, + }, + }, + }) + ) + ) + } + //Write Comment + await Promise.all( + pagesToUpdateBatch.map(({ pageId, ...pr }) => + notion.comments.create({ + parent: { + page_id: pr.page_id, + }, + rich_text: [ + { + type: "text", + text: { + content: "Your PR", + link: { + url: pr.pr_link, + }, + }, + annotations: { + bold: true, + }, + }, + { + type: "text", + text: { + content: pr.comment_content, + }, + }, + ], + }) + ) + ) + } + if (pagesToUpdate.length == 0) { + console.log("Notion Tasks are already up-to-date") + } else { + console.log( + "Successfully updated " + pagesToUpdate.length + " task(s) in Notion" + ) + } +} diff --git a/examples/notion-task-github-pr-sync/package.json b/examples/notion-task-github-pr-sync/package.json new file mode 100644 index 00000000..78802930 --- /dev/null +++ b/examples/notion-task-github-pr-sync/package.json @@ -0,0 +1,21 @@ +{ + "name": "notion-task-github-pr-sync", + "version": "1.0.0", + "description": "Sync Notion tasks with Github PRs", + "main": "index.js", + "engines": { + "node": "12.x" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "" + }, + "author": "Sid Verma", + "license": "MIT", + "dependencies": { + "@notionhq/client": "file:../../", + "dotenv": "^16.0.1", + "lodash": "^4.17.21", + "octokit": "^2.0.3" + } +} diff --git a/examples/parse-text-from-any-block-type/.env.example b/examples/parse-text-from-any-block-type/.env.example new file mode 100644 index 00000000..b30c6ebf --- /dev/null +++ b/examples/parse-text-from-any-block-type/.env.example @@ -0,0 +1,2 @@ +NOTION_KEY= +NOTION_PAGE_ID= \ No newline at end of file diff --git a/examples/parse-text-from-any-block-type/.prettierrc b/examples/parse-text-from-any-block-type/.prettierrc new file mode 100644 index 00000000..302f65ea --- /dev/null +++ b/examples/parse-text-from-any-block-type/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "avoid", + "tabWidth": 2, + "semi": false, + "trailingComma": "es5", + "endOfLine": "lf", + "singleQuote": false +} diff --git a/examples/parse-text-from-any-block-type/README.md b/examples/parse-text-from-any-block-type/README.md new file mode 100644 index 00000000..86be968b --- /dev/null +++ b/examples/parse-text-from-any-block-type/README.md @@ -0,0 +1,63 @@ +# Sample integration: Parse text from any block type + +Parse plain text from any type of block (i.e. page content), including headers, lists, media, etc. + +## About the integration + +This integration will retrieve Notion [page content](https://developers.notion.com/docs/working-with-page-content) and parse any plain text from the block. (Note: page content is represented by [blocks](https://developers.notion.com/docs/working-with-page-content#modeling-content-as-blocks).) The plain text is printed to the command line in this example, but can be used in your Notion projects as needed. + +## About page content + +When [retrieving block children](https://developers.notion.com/reference/get-block-children) (i.e. page content) with the public API, the structure of the blocks returned will vary depending on the block type. For example, a [paragraph block](https://developers.notion.com/reference/block#paragraph) and an [image block](https://developers.notion.com/reference/block#image) are modeled differently. + +This example demonstrates how to parse any available text for any type of block. In many cases, [rich text](https://developers.notion.com/reference/rich-text) will be available and the `plain_text` value will be used. + +Note: Not all blocks contain text to display (e.g. dividers). Additionally, not all block types are currently supported by the public API. + +## Running Locally + +### 1. Setup your local project + +```zsh +# Clone this repository locally +git clone https://github.com/makenotion/notion-sdk-js.git + +# Switch into this project +cd notion-sdk-js/examples/parse-text-from-any-block-type + +# Install the dependencies +npm install +``` + +### 2. Set your environment variables in a `.env` file + +A `.env.example` file has been included and can be renamed `.env`. Update the environment variables below: + +```zsh +NOTION_API_KEY= +NOTION_PAGE_ID= +``` + +`NOTION_API_KEY`: Create a new integration in the [integrations dashboard](https://www.notion.com/my-integrations) and retrieve the API key from the integration's `Secrets` page. + +`NOTION_PAGE_ID`: Use the ID of any Notion page with content. A page with a variety of block types is recommended. + +The page ID is the 32 character string at the end of any page URL. +![A Notion page URL with the ID highlighted](./assets/page_id.png) + +### 3. Give the integration access to your page + +Your Notion integration will need permission to retrieve the block children from the Notion page being used. To provide access, do the following: + +1. Go to the page in your workspace. +2. Click the `•••` (more menu) on the top-right corner of the page. +3. Scroll to the bottom of the menu and click `Add connections`. +4. Search for and select your integration in the `Search for connections...` menu. + +Once selected, your integration will have permission to read content from the page. + +### 4. Run code + +```zsh +node index.js +``` diff --git a/examples/parse-text-from-any-block-type/assets/page_id.png b/examples/parse-text-from-any-block-type/assets/page_id.png new file mode 100644 index 00000000..d0e837b6 Binary files /dev/null and b/examples/parse-text-from-any-block-type/assets/page_id.png differ diff --git a/examples/parse-text-from-any-block-type/index.js b/examples/parse-text-from-any-block-type/index.js new file mode 100644 index 00000000..2ca98c9f --- /dev/null +++ b/examples/parse-text-from-any-block-type/index.js @@ -0,0 +1,151 @@ +import { Client, iteratePaginatedAPI } from "@notionhq/client" +import { config } from "dotenv" + +config() + +const pageId = process.env.NOTION_PAGE_ID +const apiKey = process.env.NOTION_API_KEY + +const notion = new Client({ auth: apiKey }) + +/* +--------------------------------------------------------------------------- +*/ + +// Take rich text array from a block child that supports rich text and return the plain text. +// Note: All rich text objects include a plain_text field. +const getPlainTextFromRichText = richText => { + return richText.map(t => t.plain_text).join("") + // Note: A page mention will return "Undefined" as the page name if the page has not been shared with the integration. See: https://developers.notion.com/reference/block#mention +} + +// Use the source URL and optional caption from media blocks (file, video, etc.) +const getMediaSourceText = block => { + let source, caption + + if (block[block.type].external) { + source = block[block.type].external.url + } else if (block[block.type].file) { + source = block[block.type].file.url + } else if (block[block.type].url) { + source = block[block.type].url + } else { + source = "[Missing case for media block types]: " + block.type + } + // If there's a caption, return it with the source + if (block[block.type].caption.length) { + caption = getPlainTextFromRichText(block[block.type].caption) + return caption + ": " + source + } + // If no caption, just return the source URL + return source +} + +// Get the plain text from any block type supported by the public API. +const getTextFromBlock = block => { + let text + + // Get rich text from blocks that support it + if (block[block.type].rich_text) { + // This will be an empty string if it's an empty line. + text = getPlainTextFromRichText(block[block.type].rich_text) + } + // Get text for block types that don't have rich text + else { + switch (block.type) { + case "unsupported": + // The public API does not support all block types yet + text = "[Unsupported block type]" + break + case "bookmark": + text = block.bookmark.url + break + case "child_database": + text = block.child_database.title + // Use "Query a database" endpoint to get db rows: https://developers.notion.com/reference/post-database-query + // Use "Retrieve a database" endpoint to get additional properties: https://developers.notion.com/reference/retrieve-a-database + break + case "child_page": + text = block.child_page.title + break + case "embed": + case "video": + case "file": + case "image": + case "pdf": + text = getMediaSourceText(block) + break + case "equation": + text = block.equation.expression + break + case "link_preview": + text = block.link_preview.url + break + case "synced_block": + // Provides ID for block it's synced with. + text = block.synced_block.synced_from + ? "This block is synced with a block with the following ID: " + + block.synced_block.synced_from[block.synced_block.synced_from.type] + : "Source sync block that another blocked is synced with." + break + case "table": + // Only contains table properties. + // Fetch children blocks for more details. + text = "Table width: " + block.table.table_width + break + case "table_of_contents": + // Does not include text from ToC; just the color + text = "ToC color: " + block.table_of_contents.color + break + case "breadcrumb": + case "column_list": + case "divider": + text = "No text available" + break + default: + text = "[Needs case added]" + break + } + } + // Blocks with the has_children property will require fetching the child blocks. (Not included in this example.) + // e.g. nested bulleted lists + if (block.has_children) { + // For now, we'll just flag there are children blocks. + text = text + " (Has children)" + } + // Includes block type for readability. Update formatting as needed. + return block.type + ": " + text +} + +async function retrieveBlockChildren(id) { + console.log("Retrieving blocks (async)...") + const blocks = [] + + // Use iteratePaginatedAPI helper function to get all blocks first-level blocks on the page + for await (const block of iteratePaginatedAPI(notion.blocks.children.list, { + block_id: id, // A page ID can be passed as a block ID: https://developers.notion.com/docs/working-with-page-content#modeling-content-as-blocks + })) { + blocks.push(block) + } + + return blocks +} + +const printBlockText = blocks => { + console.log("Displaying blocks:") + + for (let i = 0; i < blocks.length; i++) { + const text = getTextFromBlock(blocks[i]) + // Print plain text for each block. + console.log(text) + } +} + +async function main() { + // Make API call to retrieve all block children from the page provided in .env + const blocks = await retrieveBlockChildren(pageId) + // Get and print plain text for each block. + printBlockText(blocks) +} + +main() diff --git a/examples/parse-text-from-any-block-type/package.json b/examples/parse-text-from-any-block-type/package.json new file mode 100644 index 00000000..1a201945 --- /dev/null +++ b/examples/parse-text-from-any-block-type/package.json @@ -0,0 +1,23 @@ +{ + "name": "parse-text-from-any-block-type", + "version": "1.0.0", + "description": "Retrieve blocks from a Notion page and parse text from any block type.", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/makenotion/notion-sdk-js.git" + }, + "author": "Jess Mitchell", + "license": "MIT", + "bugs": { + "url": "https://github.com/makenotion/notion-sdk-js/issues" + }, + "dependencies": { + "@notionhq/client": "file:../.." + } +} diff --git a/examples/web-form-with-express/.env.example b/examples/web-form-with-express/.env.example new file mode 100644 index 00000000..b30c6ebf --- /dev/null +++ b/examples/web-form-with-express/.env.example @@ -0,0 +1,2 @@ +NOTION_KEY= +NOTION_PAGE_ID= \ No newline at end of file diff --git a/examples/web-form-with-express/README.md b/examples/web-form-with-express/README.md new file mode 100644 index 00000000..ce4d2014 --- /dev/null +++ b/examples/web-form-with-express/README.md @@ -0,0 +1,114 @@ +# Notion internal integration full-stack example + +## Use web forms to create new databases, pages, page content, and comments + +### About the integration + +This demo shows how to build an internal integration that allows users to fill out a web form to create new Notion databases, pages, blocks (page content), and comments. + +![Database form submitted successfully](./public/assets/home-screen.png) + +This demo is referenced in the [Create an integration guide](https://developers.notion.com/docs/create-a-notion-integration) -- an introductory guide to building internal integrations and working with Notion's public API. + +The goal of this integration is to show how to build a full-stack app where user interactions on the frontend will trigger public API requests, and, as a result, make the corresponding updates in your Notion workspace. + +### File structure + +On the frontend, this demo includes: + +- `views/index.html`, which represents the app's webpage content. Users will interact with the HTML elements in this page. +- `public/client.js`, the client-side JavaScript added to handle HTML form `submit` events. +- `public/style.css` contains the styles for `views/index.html`. + +On the backend, this demo includes: + +- `server.js`, which serves `index.html` and defines the endpoints used in the client-side JS code. All Notion public API usage (Notion SDK for JavaScript) is included in this file. + +#### Notion endpoints used + +This demo includes the following Notion endpoint usage: + +- [Create a database](https://developers.notion.com/reference/create-a-database) +- [Create a page](https://developers.notion.com/reference/post-page) +- [Append block children](https://developers.notion.com/reference/patch-block-children) +- [Create a comment](https://developers.notion.com/reference/create-a-comment) + +This demo can be expanded further to test other endpoints, as well. For example, you could add a button retrieve all database pages or to delete existing pages. + +Some "real-world" applications include expanding this demo to be a blog and using a Notion workspace as a CMS. Additionally, the functionality could also be repurposed to receive app feedback from users. + +--- + +### Running locally + +#### 1. Set up your local project + +```zsh +# Clone this repository locally +git clone https://github.com/makenotion/notion-sdk-js.git + +# Switch into this project +cd notion-sdk-js/examples/web-form-with-express/ + +# Install the dependencies +npm install +``` + +#### 2. Set your environment variables in a `.env` file + +A `.env.example` file has been included and can be renamed `.env`. Update the environment variables below: + +```zsh +NOTION_KEY= +NOTION_PAGE_ID= +``` + +`NOTION_KEY`: Create a new integration in the [integrations dashboard](https://www.notion.com/my-integrations) and retrieve the API key from the integration's `Secrets` page. + +`NOTION_PAGE_ID`: Use the ID of any Notion page you want to add databases to. This page will be the parent of all content created through this integration. + +The page ID is the 32 character string at the end of any page URL. +![A Notion page URL with the ID highlighted](./public/assets//page_id.png) + +#### 3. Give the integration access to your page + +Your Notion integration will need permission to create new databases, etc. To provide access, do the following: + +1. Go to the page in your workspace. +2. Click the `•••` (more menu) on the top-right corner of the page. +3. Scroll to the bottom of the menu and click `Add connections`. +4. Search for and select your integration in the `Search for connections...` menu. + +Once selected, your integration will have permission to read/write content on the page. + +**Note**: For the `Add a comment` form to work, you must give your integration permission to read/write comments. To update integration capabilities, visit the `Capabilities` tab in the [integrations dashboard](https://www.notion.com/my-integrations). + +#### 4. Run code + +Run the following command: + +```zsh +node server.js +``` + +Check the terminal response to see which port to use when viewing the app locally in your browser of choice (`localhost:`). + +Keep the browser console open to see API responses, including errors. + +### Using this app + +To use this demo app, start by creating a new database via the database form: +![Database form UI](./public/assets/home-screen.png) + +The ID of the new database can be used in the next form to create a new page: +![Page form UI](./public/assets/page-form.png) + +The blocks and comment forms will accept the page ID that is returned from the page form to create new page content (blocks) and comments. +![Blocks form UI](./public/assets/blocks-form.png) +![Comments form UI](./public/assets/comment-form.png) + +If you have the IDs for other databases/pages, you can use them as long as you have shared the target databases/pages with the internal integration. + +To learn more about this demo, read the [Create an integration guide](https://developers.notion.com/docs/create-a-notion-integration), which steps through how this code works. + +(Thanks to [Glitch](https://glitch.com/) for the starter app used while creating this demo!) diff --git a/examples/web-form-with-express/package.json b/examples/web-form-with-express/package.json new file mode 100644 index 00000000..ad5b9581 --- /dev/null +++ b/examples/web-form-with-express/package.json @@ -0,0 +1,31 @@ +{ + "name": "web-form-with-express-example", + "version": "1.0.0", + "description": "Internal Notion intergration demo app using Express.js for a server.", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "@notionhq/client": "^2.2.7", + "dotenv": "^16.3.1", + "express": "^4.18.2" + }, + "engines": { + "node": "14.x" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/makenotion/notion-sdk-js.git" + }, + "author": "Jess Mitchell", + "license": "MIT", + "bugs": { + "url": "https://github.com/makenotion/notion-sdk-js/issues" + }, + "keywords": [ + "node", + "notion", + "express" + ] +} diff --git a/examples/web-form-with-express/public/assets/blocks-form.png b/examples/web-form-with-express/public/assets/blocks-form.png new file mode 100644 index 00000000..2716e3af Binary files /dev/null and b/examples/web-form-with-express/public/assets/blocks-form.png differ diff --git a/examples/web-form-with-express/public/assets/comment-form.png b/examples/web-form-with-express/public/assets/comment-form.png new file mode 100644 index 00000000..f6a476cc Binary files /dev/null and b/examples/web-form-with-express/public/assets/comment-form.png differ diff --git a/examples/web-form-with-express/public/assets/home-screen.png b/examples/web-form-with-express/public/assets/home-screen.png new file mode 100644 index 00000000..ad3b4ba1 Binary files /dev/null and b/examples/web-form-with-express/public/assets/home-screen.png differ diff --git a/examples/web-form-with-express/public/assets/page-form.png b/examples/web-form-with-express/public/assets/page-form.png new file mode 100644 index 00000000..5097dbfc Binary files /dev/null and b/examples/web-form-with-express/public/assets/page-form.png differ diff --git a/examples/web-form-with-express/public/assets/page_id.png b/examples/web-form-with-express/public/assets/page_id.png new file mode 100644 index 00000000..d0e837b6 Binary files /dev/null and b/examples/web-form-with-express/public/assets/page_id.png differ diff --git a/examples/web-form-with-express/public/client.js b/examples/web-form-with-express/public/client.js new file mode 100644 index 00000000..0cc9ddc2 --- /dev/null +++ b/examples/web-form-with-express/public/client.js @@ -0,0 +1,142 @@ +// This file is run by the browser each time your view template is loaded + +/** + * Define variables that reference elements included in /views/index.html: + */ + +// Forms +const dbForm = document.getElementById("databaseForm") +const pageForm = document.getElementById("pageForm") +const blocksForm = document.getElementById("blocksForm") +const commentForm = document.getElementById("commentForm") + +// Table cells where API responses will be appended +const dbResponseEl = document.getElementById("dbResponse") +const pageResponseEl = document.getElementById("pageResponse") +const blocksResponseEl = document.getElementById("blocksResponse") +const commentResponseEl = document.getElementById("commentResponse") + +/** + * Functions to handle appending new content to /views/index.html + */ + +// Appends the API response to the UI +const appendApiResponse = function (apiResponse, el) { + console.log(apiResponse) + + // Add success message to UI + const newParagraphSuccessMsg = document.createElement("p") + newParagraphSuccessMsg.innerHTML = "Result: " + apiResponse.message + el.appendChild(newParagraphSuccessMsg) + // See browser console for more information + if (apiResponse.message === "error") return + + // Add ID of Notion item (db, page, comment) to UI + const newParagraphId = document.createElement("p") + newParagraphId.innerHTML = "ID: " + apiResponse.data.id + el.appendChild(newParagraphId) + + // Add URL of Notion item (db, page) to UI + if (apiResponse.data.url) { + const newAnchorTag = document.createElement("a") + newAnchorTag.setAttribute("href", apiResponse.data.url) + newAnchorTag.innerText = apiResponse.data.url + el.appendChild(newAnchorTag) + } +} + +// Appends the blocks API response to the UI +const appendBlocksResponse = function (apiResponse, el) { + console.log(apiResponse) + + // Add success message to UI + const newParagraphSuccessMsg = document.createElement("p") + newParagraphSuccessMsg.innerHTML = "Result: " + apiResponse.message + el.appendChild(newParagraphSuccessMsg) + + // Add block ID to UI + const newParagraphId = document.createElement("p") + newParagraphId.innerHTML = "ID: " + apiResponse.data.results[0].id + el.appendChild(newParagraphId) +} + +/** + * Attach submit event handlers to each form included in /views/index.html + */ + +// Attach submit event to each form +dbForm.onsubmit = async function (event) { + event.preventDefault() + + const dbName = event.target.dbName.value + const body = JSON.stringify({ dbName }) + + const newDBResponse = await fetch("/databases", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }) + const newDBData = await newDBResponse.json() + + appendApiResponse(newDBData, dbResponseEl) +} + +pageForm.onsubmit = async function (event) { + event.preventDefault() + + const dbID = event.target.newPageDB.value + const pageName = event.target.newPageName.value + const header = event.target.header.value + const body = JSON.stringify({ dbID, pageName, header }) + + const newPageResponse = await fetch("/pages", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }) + + const newPageData = await newPageResponse.json() + appendApiResponse(newPageData, pageResponseEl) +} + +blocksForm.onsubmit = async function (event) { + event.preventDefault() + + const pageID = event.target.pageID.value + const content = event.target.content.value + const body = JSON.stringify({ pageID, content }) + + const newBlockResponse = await fetch("/blocks", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }) + + const newBlockData = await newBlockResponse.json() + appendBlocksResponse(newBlockData, blocksResponseEl) +} + +commentForm.onsubmit = async function (event) { + event.preventDefault() + + const pageID = event.target.pageIDComment.value + const comment = event.target.comment.value + const body = JSON.stringify({ pageID, comment }) + + const newCommentResponse = await fetch("/comments", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body, + }) + + const newCommentData = await newCommentResponse.json() + appendApiResponse(newCommentData, commentResponseEl) +} diff --git a/examples/web-form-with-express/public/style.css b/examples/web-form-with-express/public/style.css new file mode 100644 index 00000000..fc0316bb --- /dev/null +++ b/examples/web-form-with-express/public/style.css @@ -0,0 +1,74 @@ +/* this file is loaded by index.html and styles the page */ + +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 2em 1em; + line-height: 1.5em; +} + +h1 { + color: #7171d1; + max-width: calc(100% - 5rem); + line-height: 1.1; +} + +table { + width: 100%; +} + +th { + text-align: left; +} + +td { + width: 50%; + word-break: break-all; +} + +form { + background-color: #eee; + display: grid; + grid-gap: 1em; + padding: 1em; + max-width: 40ch; +} + +input { + border: 1px solid silver; + display: block; + font-size: 16px; + margin-bottom: 10px; + padding: 5px; + width: 100%; +} + +form input[type="submit"] { + background-color: #bbbbf2; + border: 2px solid currentColor; + border-radius: 0.25em; + cursor: pointer; + font-size: inherit; + line-height: 1.4em; + padding: 0.25em 1em; + max-width: 20ch; + cursor: pointer; + margin-left: auto; +} + +form input[type="submit"]:hover { + background-color: #9292d3; +} + +form input[type="submit"]:active { + background-color: #b2b2d5; +} + +footer { + margin-top: 3em; + padding-top: 1.5em; + border-top: 1px solid lightgrey; +} diff --git a/examples/web-form-with-express/server.js b/examples/web-form-with-express/server.js new file mode 100644 index 00000000..ba1eb12f --- /dev/null +++ b/examples/web-form-with-express/server.js @@ -0,0 +1,144 @@ +require("dotenv").config() +const express = require("express") +const app = express() + +const { Client } = require("@notionhq/client") +const notion = new Client({ auth: process.env.NOTION_KEY }) + +// http://expressjs.com/en/starter/static-files.html +app.use(express.static("public")) +app.use(express.json()) // for parsing application/json + +// http://expressjs.com/en/starter/basic-routing.html +app.get("/", function (request, response) { + response.sendFile(__dirname + "/views/index.html") +}) + +// Create new database. The page ID is set in the environment variables. +app.post("/databases", async function (request, response) { + const pageId = process.env.NOTION_PAGE_ID + const title = request.body.dbName + + try { + const newDb = await notion.databases.create({ + parent: { + type: "page_id", + page_id: pageId, + }, + title: [ + { + type: "text", + text: { + content: title, + }, + }, + ], + properties: { + Name: { + title: {}, + }, + }, + }) + response.json({ message: "success!", data: newDb }) + } catch (error) { + response.json({ message: "error", error }) + } +}) + +// Create new page. The database ID is provided in the web form. +app.post("/pages", async function (request, response) { + const { dbID, pageName, header } = request.body + + try { + const newPage = await notion.pages.create({ + parent: { + type: "database_id", + database_id: dbID, + }, + properties: { + Name: { + title: [ + { + text: { + content: pageName, + }, + }, + ], + }, + }, + children: [ + { + object: "block", + heading_2: { + rich_text: [ + { + text: { + content: header, + }, + }, + ], + }, + }, + ], + }) + response.json({ message: "success!", data: newPage }) + } catch (error) { + response.json({ message: "error", error }) + } +}) + +// Create new block (page content). The page ID is provided in the web form. +app.post("/blocks", async function (request, response) { + const { pageID, content } = request.body + + try { + const newBlock = await notion.blocks.children.append({ + block_id: pageID, // a block ID can be a page ID + children: [ + { + // Use a paragraph as a default but the form or request can be updated to allow for other block types: https://developers.notion.com/reference/block#keys + paragraph: { + rich_text: [ + { + text: { + content: content, + }, + }, + ], + }, + }, + ], + }) + response.json({ message: "success!", data: newBlock }) + } catch (error) { + response.json({ message: "error", error }) + } +}) + +// Create new page comments. The page ID is provided in the web form. +app.post("/comments", async function (request, response) { + const { pageID, comment } = request.body + + try { + const newComment = await notion.comments.create({ + parent: { + page_id: pageID, + }, + rich_text: [ + { + text: { + content: comment, + }, + }, + ], + }) + response.json({ message: "success!", data: newComment }) + } catch (error) { + response.json({ message: "error", error }) + } +}) + +// listen for requests :) +const listener = app.listen(process.env.PORT, function () { + console.log("Your app is listening on port " + listener.address().port) +}) diff --git a/examples/web-form-with-express/views/index.html b/examples/web-form-with-express/views/index.html new file mode 100644 index 00000000..9cb6e1b3 --- /dev/null +++ b/examples/web-form-with-express/views/index.html @@ -0,0 +1,96 @@ + + + + + + + + + + Notion integration demo + + + + + + + +
+

Notion internal integration demo

+
+ +
+

Goals

+

+ Use this demo to see how to incorporate Notion's public API into your + app. Build on this demo as needed to meet your own app's requirements! +

+

+ Open the Notion page set in your environment variables to see workspace + updates in real-time. :) +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
StepResponse
+

1. Create a new database

+
+ + + +
+
+

2. Add a page to the database

+
+ + + + + + + +
+
+

3. Add content to the page

+
+ + + + + +
+
+

4. Add a comment to the content

+
+ + + + + +
+
+
+ + diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..63a2994a --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", +} diff --git a/package.json b/package.json index 3eba23c9..51ca65e4 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,58 @@ { - "name": "@notion/client", - "version": "0.1.0", - "description": "", + "name": "@notionhq/client", + "version": "2.2.13", + "description": "A simple and easy to use client for the Notion API", "engines": { - "node": ">=14.15.0" + "node": ">=12" }, + "homepage": "https://developers.notion.com/docs/getting-started", + "bugs": { + "url": "https://github.com/makenotion/notion-sdk-js/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/makenotion/notion-sdk-js/" + }, + "keywords": [ + "notion", + "notionapi", + "rest", + "notion-api" + ], "main": "./build/src", + "types": "./build/src/index.d.ts", "scripts": { "prepare": "npm run build", + "prepublishOnly": "npm run checkLoggedIn && npm run lint && npm run test", "build": "tsc", - "lint": "eslint . --ext .ts", - "test": "ava" + "prettier": "prettier --write .", + "lint": "prettier --check . && eslint . --ext .ts && cspell '**/*' ", + "test": "jest ./test", + "check-links": "git ls-files | grep md$ | xargs -n 1 markdown-link-check", + "prebuild": "npm run clean", + "clean": "rm -rf ./build", + "checkLoggedIn": "./scripts/verifyLoggedIn.sh" }, "author": "", "license": "MIT", - "files": ["build/src/**"], + "files": [ + "build/package.json", + "build/src/**" + ], "dependencies": { - "got": "^11.8.2" + "@types/node-fetch": "^2.5.10", + "node-fetch": "^2.6.1" }, "devDependencies": { - "@ava/typescript": "^2.0.0", - "@typescript-eslint/eslint-plugin": "^4.22.0", - "@typescript-eslint/parser": "^4.22.0", - "ava": "^3.15.0", + "@types/jest": "^28.1.4", + "@typescript-eslint/eslint-plugin": "^5.39.0", + "@typescript-eslint/parser": "^5.39.0", + "cspell": "^5.4.1", "eslint": "^7.24.0", - "typescript": "^4.2.4" + "jest": "^28.1.2", + "markdown-link-check": "^3.8.7", + "prettier": "^2.8.8", + "ts-jest": "^28.0.5", + "typescript": "^4.8.4" } } diff --git a/scripts/verifyLoggedIn.sh b/scripts/verifyLoggedIn.sh new file mode 100755 index 00000000..4689bd13 --- /dev/null +++ b/scripts/verifyLoggedIn.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +############################################################################## +# +# Verify the user is logged into NPM. +# +# Useful when publishing to NPM. +# +############################################################################## + +npm whoami &> /dev/null +if [[ $? -ne 0 ]]; then + printf "❌ Failed: Please login to NPM before publishing.\n\n"; + exit 1; +fi diff --git a/src/Client.ts b/src/Client.ts index 438fc7bb..db935575 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,49 +1,142 @@ -import got, { Got, Options as GotOptions, Headers as GotHeaders } from 'got'; -import { LogLevel, logLevelSeverity } from './logging'; -import { buildRequestError, HTTPResponseError } from './errors' -import { pick } from './helpers'; +import type { Agent } from "http" import { - DatabasesRetrieveParameters, DatabasesRetrieveResponse, databasesRetrieve, - DatabasesQueryResponse, DatabasesQueryParameters, databasesQuery, -} from './api-endpoints'; + Logger, + LogLevel, + logLevelSeverity, + makeConsoleLogger, +} from "./logging" +import { + buildRequestError, + isHTTPResponseError, + isNotionClientError, + RequestTimeoutError, +} from "./errors" +import { pick } from "./utils" +import { + GetBlockParameters, + GetBlockResponse, + getBlock, + UpdateBlockParameters, + UpdateBlockResponse, + updateBlock, + DeleteBlockParameters, + DeleteBlockResponse, + deleteBlock, + AppendBlockChildrenParameters, + AppendBlockChildrenResponse, + appendBlockChildren, + ListBlockChildrenParameters, + ListBlockChildrenResponse, + listBlockChildren, + ListDatabasesParameters, + ListDatabasesResponse, + listDatabases, + GetDatabaseParameters, + GetDatabaseResponse, + getDatabase, + QueryDatabaseParameters, + QueryDatabaseResponse, + queryDatabase, + CreateDatabaseParameters, + CreateDatabaseResponse, + createDatabase, + UpdateDatabaseParameters, + UpdateDatabaseResponse, + updateDatabase, + CreatePageParameters, + CreatePageResponse, + createPage, + GetPageParameters, + GetPageResponse, + getPage, + UpdatePageParameters, + UpdatePageResponse, + updatePage, + GetUserParameters, + GetUserResponse, + getUser, + ListUsersParameters, + ListUsersResponse, + listUsers, + SearchParameters, + SearchResponse, + search, + GetSelfParameters, + GetSelfResponse, + getSelf, + GetPagePropertyParameters, + GetPagePropertyResponse, + getPageProperty, + CreateCommentParameters, + CreateCommentResponse, + createComment, + ListCommentsParameters, + ListCommentsResponse, + listComments, + OauthTokenResponse, + OauthTokenParameters, + oauthToken, +} from "./api-endpoints" +import nodeFetch from "node-fetch" +import { + version as PACKAGE_VERSION, + name as PACKAGE_NAME, +} from "../package.json" +import { SupportedFetch } from "./fetch-types" export interface ClientOptions { - auth?: string; - timeoutMs?: number; - baseUrl?: string; - logLevel?: LogLevel; + auth?: string + timeoutMs?: number + baseUrl?: string + logLevel?: LogLevel + logger?: Logger + notionVersion?: string + fetch?: SupportedFetch + /** Silently ignored in the browser */ + agent?: Agent } export interface RequestParameters { - path: string; - method: Method; - query?: QueryParams; - body?: Record; - auth?: string; + path: string + method: Method + query?: QueryParams + body?: Record + /** + * To authenticate using public API token, `auth` should be passed as a + * string. If you are trying to complete OAuth, then `auth` should be an object + * containing your integration's client ID and secret. + */ + auth?: + | string + | { + client_id: string + client_secret: string + } } export default class Client { + #auth?: string + #logLevel: LogLevel + #logger: Logger + #prefixUrl: string + #timeoutMs: number + #notionVersion: string + #fetch: SupportedFetch + #agent: Agent | undefined + #userAgent: string - #auth?: string; - #logLevel: LogLevel; - #got: Got; + static readonly defaultNotionVersion = "2022-06-28" public constructor(options?: ClientOptions) { - this.#auth = options?.auth; - this.#logLevel = options?.logLevel ?? LogLevel.WARN; - - const prefixUrl = (options?.baseUrl ?? 'https://api.notion.com') + '/v1/'; - const timeout = options?.timeoutMs ?? 60_000; - - this.#got = got.extend({ - prefixUrl, - timeout, - headers: { - // TODO: update with format appropriate for telemetry, use version from package.json - 'user-agent': 'notion:client/v0.1.0', - }, - retry: 0, - }); + this.#auth = options?.auth + this.#logLevel = options?.logLevel ?? LogLevel.WARN + this.#logger = options?.logger ?? makeConsoleLogger(PACKAGE_NAME) + this.#prefixUrl = (options?.baseUrl ?? "https://api.notion.com") + "/v1/" + this.#timeoutMs = options?.timeoutMs ?? 60_000 + this.#notionVersion = options?.notionVersion ?? Client.defaultNotionVersion + this.#fetch = options?.fetch ?? nodeFetch + this.#agent = options?.agent + this.#userAgent = `notionhq-client/${PACKAGE_VERSION}` } /** @@ -55,37 +148,100 @@ export default class Client { * @param body * @returns */ - public async request({ path, method, query, body, auth }: RequestParameters): Promise { - this.log(LogLevel.INFO, `request start`, { method, path }); + public async request({ + path, + method, + query, + body, + auth, + }: RequestParameters): Promise { + this.log(LogLevel.INFO, "request start", { method, path }) // If the body is empty, don't send the body in the HTTP request - const json = (body !== undefined && Object.entries(body).length === 0) ? undefined : body; + const bodyAsJsonString = + !body || Object.entries(body).length === 0 + ? undefined + : JSON.stringify(body) + const url = new URL(`${this.#prefixUrl}${path}`) + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) { + if (Array.isArray(value)) { + value.forEach(val => + url.searchParams.append(key, decodeURIComponent(val)) + ) + } else { + url.searchParams.append(key, String(value)) + } + } + } + } + + // Allow both client ID / client secret based auth as well as token based auth. + let authorizationHeader: Record + if (typeof auth === "object") { + // Client ID and secret based auth is **ONLY** supported when using the + // `/oauth/token` endpoint. If this is the case, handle formatting the + // authorization header as required by `Basic` auth. + const unencodedCredential = `${auth.client_id}:${auth.client_secret}` + const encodedCredential = + typeof Buffer !== "undefined" + ? Buffer.from(unencodedCredential).toString("base64") + : btoa(unencodedCredential) + authorizationHeader = { authorization: `Basic ${encodedCredential}` } + } else { + // Otherwise format authorization header as `Bearer` token auth. + authorizationHeader = this.authAsHeaders(auth) + } + + const headers: Record = { + ...authorizationHeader, + "Notion-Version": this.#notionVersion, + "user-agent": this.#userAgent, + } + + if (bodyAsJsonString !== undefined) { + headers["content-type"] = "application/json" + } try { - const response = await this.#got(path, { - method, - searchParams: query, - json, - headers: this.authAsHeaders(auth), - }).json(); - - this.log(LogLevel.INFO, `request success`, { method, path }); - return response; - } catch (error) { - // Build an error of a known type, otherwise throw unexpected errors - const requestError = buildRequestError(error); - if (requestError === undefined) { - throw error; + const response = await RequestTimeoutError.rejectAfterTimeout( + this.#fetch(url.toString(), { + method: method.toUpperCase(), + headers, + body: bodyAsJsonString, + agent: this.#agent, + }), + this.#timeoutMs + ) + + const responseText = await response.text() + if (!response.ok) { + throw buildRequestError(response, responseText) } - this.log(LogLevel.WARN, `request fail`, { code: requestError.code, message: requestError.message }); - if (HTTPResponseError.isHTTPResponseError(requestError)) { + const responseJson: ResponseBody = JSON.parse(responseText) + this.log(LogLevel.INFO, `request success`, { method, path }) + return responseJson + } catch (error: unknown) { + if (!isNotionClientError(error)) { + throw error + } + + // Log the error if it's one of our known error types + this.log(LogLevel.WARN, `request fail`, { + code: error.code, + message: error.message, + }) + + if (isHTTPResponseError(error)) { // The response body may contain sensitive information so it is logged separately at the DEBUG level - this.log(LogLevel.DEBUG, `failed response body`, { body: requestError.body }); + this.log(LogLevel.DEBUG, `failed response body`, { + body: error.body, + }) } - // Throw as a known error type - throw requestError; + throw error } } @@ -93,33 +249,334 @@ export default class Client { * Notion API endpoints */ + public readonly blocks = { + /** + * Retrieve block + */ + retrieve: ( + args: WithAuth + ): Promise => { + return this.request({ + path: getBlock.path(args), + method: getBlock.method, + query: pick(args, getBlock.queryParams), + body: pick(args, getBlock.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Update block + */ + update: ( + args: WithAuth + ): Promise => { + return this.request({ + path: updateBlock.path(args), + method: updateBlock.method, + query: pick(args, updateBlock.queryParams), + body: pick(args, updateBlock.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Delete block + */ + delete: ( + args: WithAuth + ): Promise => { + return this.request({ + path: deleteBlock.path(args), + method: deleteBlock.method, + query: pick(args, deleteBlock.queryParams), + body: pick(args, deleteBlock.bodyParams), + auth: args?.auth, + }) + }, + children: { + /** + * Append block children + */ + append: ( + args: WithAuth + ): Promise => { + return this.request({ + path: appendBlockChildren.path(args), + method: appendBlockChildren.method, + query: pick(args, appendBlockChildren.queryParams), + body: pick(args, appendBlockChildren.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Retrieve block children + */ + list: ( + args: WithAuth + ): Promise => { + return this.request({ + path: listBlockChildren.path(args), + method: listBlockChildren.method, + query: pick(args, listBlockChildren.queryParams), + body: pick(args, listBlockChildren.bodyParams), + auth: args?.auth, + }) + }, + }, + } + public readonly databases = { + /** + * List databases + * + * @deprecated Please use `search` + */ + list: ( + args: WithAuth + ): Promise => { + return this.request({ + path: listDatabases.path(), + method: listDatabases.method, + query: pick(args, listDatabases.queryParams), + body: pick(args, listDatabases.bodyParams), + auth: args?.auth, + }) + }, + /** * Retrieve a database */ - retrieve: (args: WithAuth): Promise => { - return this.request({ - path: databasesRetrieve.path(args), - method: databasesRetrieve.method, - query: pick(args, databasesRetrieve.queryParams), - body: pick(args, databasesRetrieve.bodyParams), - auth: args.auth, - }); + retrieve: ( + args: WithAuth + ): Promise => { + return this.request({ + path: getDatabase.path(args), + method: getDatabase.method, + query: pick(args, getDatabase.queryParams), + body: pick(args, getDatabase.bodyParams), + auth: args?.auth, + }) }, /** * Query a database */ - query: (args: WithAuth): Promise => { - return this.request({ - path: databasesQuery.path(args), - method: databasesQuery.method, - query: pick(args, databasesQuery.queryParams), - body: pick(args, databasesQuery.bodyParams), - auth: args.auth, - }); + query: ( + args: WithAuth + ): Promise => { + return this.request({ + path: queryDatabase.path(args), + method: queryDatabase.method, + query: pick(args, queryDatabase.queryParams), + body: pick(args, queryDatabase.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Create a database + */ + create: ( + args: WithAuth + ): Promise => { + return this.request({ + path: createDatabase.path(), + method: createDatabase.method, + query: pick(args, createDatabase.queryParams), + body: pick(args, createDatabase.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Update a database + */ + update: ( + args: WithAuth + ): Promise => { + return this.request({ + path: updateDatabase.path(args), + method: updateDatabase.method, + query: pick(args, updateDatabase.queryParams), + body: pick(args, updateDatabase.bodyParams), + auth: args?.auth, + }) }, - }; + } + + public readonly pages = { + /** + * Create a page + */ + create: ( + args: WithAuth + ): Promise => { + return this.request({ + path: createPage.path(), + method: createPage.method, + query: pick(args, createPage.queryParams), + body: pick(args, createPage.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Retrieve a page + */ + retrieve: (args: WithAuth): Promise => { + return this.request({ + path: getPage.path(args), + method: getPage.method, + query: pick(args, getPage.queryParams), + body: pick(args, getPage.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Update page properties + */ + update: ( + args: WithAuth + ): Promise => { + return this.request({ + path: updatePage.path(args), + method: updatePage.method, + query: pick(args, updatePage.queryParams), + body: pick(args, updatePage.bodyParams), + auth: args?.auth, + }) + }, + properties: { + /** + * Retrieve page property + */ + retrieve: ( + args: WithAuth + ): Promise => { + return this.request({ + path: getPageProperty.path(args), + method: getPageProperty.method, + query: pick(args, getPageProperty.queryParams), + body: pick(args, getPageProperty.bodyParams), + auth: args?.auth, + }) + }, + }, + } + + public readonly users = { + /** + * Retrieve a user + */ + retrieve: (args: WithAuth): Promise => { + return this.request({ + path: getUser.path(args), + method: getUser.method, + query: pick(args, getUser.queryParams), + body: pick(args, getUser.bodyParams), + auth: args?.auth, + }) + }, + + /** + * List all users + */ + list: (args: WithAuth): Promise => { + return this.request({ + path: listUsers.path(), + method: listUsers.method, + query: pick(args, listUsers.queryParams), + body: pick(args, listUsers.bodyParams), + auth: args?.auth, + }) + }, + + /** + * Get details about bot + */ + me: (args: WithAuth): Promise => { + return this.request({ + path: getSelf.path(), + method: getSelf.method, + query: pick(args, getSelf.queryParams), + body: pick(args, getSelf.bodyParams), + auth: args?.auth, + }) + }, + } + + public readonly comments = { + /** + * Create a comment + */ + create: ( + args: WithAuth + ): Promise => { + return this.request({ + path: createComment.path(), + method: createComment.method, + query: pick(args, createComment.queryParams), + body: pick(args, createComment.bodyParams), + auth: args?.auth, + }) + }, + + /** + * List comments + */ + list: ( + args: WithAuth + ): Promise => { + return this.request({ + path: listComments.path(), + method: listComments.method, + query: pick(args, listComments.queryParams), + body: pick(args, listComments.bodyParams), + auth: args?.auth, + }) + }, + } + + /** + * Search + */ + public search = ( + args: WithAuth + ): Promise => { + return this.request({ + path: search.path(), + method: search.method, + query: pick(args, search.queryParams), + body: pick(args, search.bodyParams), + auth: args?.auth, + }) + } + + public readonly oauth = { + /** + * Get token + */ + token: ( + args: OauthTokenParameters & { + client_id: string + client_secret: string + } + ): Promise => { + return this.request({ + path: oauthToken.path(), + method: oauthToken.method, + query: pick(args, oauthToken.queryParams), + body: pick(args, oauthToken.bodyParams), + auth: { + client_id: args.client_id, + client_secret: args.client_secret, + }, + }) + }, + } /** * Emits a log message to the console. @@ -127,9 +584,13 @@ export default class Client { * @param level The level for this message * @param args Arguments to send to the console */ - private log(level: LogLevel, ...args: unknown[]) { + private log( + level: LogLevel, + message: string, + extraInfo: Record + ) { if (logLevelSeverity(level) >= logLevelSeverity(this.#logLevel)) { - console.log(`${this.constructor.name} ${level}: `, ...args); + this.#logger(level, message, extraInfo) } } @@ -142,21 +603,20 @@ export default class Client { * @param auth API key or access token * @returns headers key-value object */ - private authAsHeaders(auth?: string): GotHeaders { - const headers: GotHeaders = {}; - const authHeaderValue = auth ?? this.#auth; + private authAsHeaders(auth?: string): Record { + const headers: Record = {} + const authHeaderValue = auth ?? this.#auth if (authHeaderValue !== undefined) { - headers['authorization'] = `Bearer ${authHeaderValue}`; + headers["authorization"] = `Bearer ${authHeaderValue}` } - return headers; + return headers } } /* * Type aliases to support the generic request interface. */ -type Method = 'get' | 'post' | 'patch'; -type QueryParams = GotOptions['searchParams']; - +type Method = "get" | "post" | "patch" | "delete" +type QueryParams = Record | URLSearchParams -type WithAuth

= P & { auth?: string }; +type WithAuth

= P & { auth?: string } diff --git a/src/api-endpoints.ts b/src/api-endpoints.ts index bd8f333e..e22d0d80 100644 --- a/src/api-endpoints.ts +++ b/src/api-endpoints.ts @@ -1,61 +1,10842 @@ -/* eslint-disable @typescript-eslint/no-empty-interface */ +// cspell:disable-file +// Note: This is a generated file. -import { NotionDatabase, NotionDatabaseFilter, NotionDatabaseSort, NotionPage, PaginatedList } from './api-types'; +type IdRequest = string | string -/** - * Notion API Endpoints - * - * This file contains metadata about each of the API endpoints such as the HTTP method, the parameters, and the types. - * In the future, the contents of this file will be generated from an API definition. - */ +export type PersonUserObjectResponse = { + type: "person" + person: { email?: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" +} + +type EmptyObject = Record + +export type PartialUserObjectResponse = { id: IdRequest; object: "user" } + +export type BotUserObjectResponse = { + type: "bot" + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" +} + +export type UserObjectResponse = + | PersonUserObjectResponse + | BotUserObjectResponse + +type StringRequest = string + +type SelectColor = + | "default" + | "gray" + | "brown" + | "orange" + | "yellow" + | "green" + | "blue" + | "purple" + | "pink" + | "red" + +type SelectPropertyResponse = { + id: StringRequest + name: StringRequest + color: SelectColor +} + +type TimeZoneRequest = + | "Africa/Abidjan" + | "Africa/Accra" + | "Africa/Addis_Ababa" + | "Africa/Algiers" + | "Africa/Asmara" + | "Africa/Asmera" + | "Africa/Bamako" + | "Africa/Bangui" + | "Africa/Banjul" + | "Africa/Bissau" + | "Africa/Blantyre" + | "Africa/Brazzaville" + | "Africa/Bujumbura" + | "Africa/Cairo" + | "Africa/Casablanca" + | "Africa/Ceuta" + | "Africa/Conakry" + | "Africa/Dakar" + | "Africa/Dar_es_Salaam" + | "Africa/Djibouti" + | "Africa/Douala" + | "Africa/El_Aaiun" + | "Africa/Freetown" + | "Africa/Gaborone" + | "Africa/Harare" + | "Africa/Johannesburg" + | "Africa/Juba" + | "Africa/Kampala" + | "Africa/Khartoum" + | "Africa/Kigali" + | "Africa/Kinshasa" + | "Africa/Lagos" + | "Africa/Libreville" + | "Africa/Lome" + | "Africa/Luanda" + | "Africa/Lubumbashi" + | "Africa/Lusaka" + | "Africa/Malabo" + | "Africa/Maputo" + | "Africa/Maseru" + | "Africa/Mbabane" + | "Africa/Mogadishu" + | "Africa/Monrovia" + | "Africa/Nairobi" + | "Africa/Ndjamena" + | "Africa/Niamey" + | "Africa/Nouakchott" + | "Africa/Ouagadougou" + | "Africa/Porto-Novo" + | "Africa/Sao_Tome" + | "Africa/Timbuktu" + | "Africa/Tripoli" + | "Africa/Tunis" + | "Africa/Windhoek" + | "America/Adak" + | "America/Anchorage" + | "America/Anguilla" + | "America/Antigua" + | "America/Araguaina" + | "America/Argentina/Buenos_Aires" + | "America/Argentina/Catamarca" + | "America/Argentina/ComodRivadavia" + | "America/Argentina/Cordoba" + | "America/Argentina/Jujuy" + | "America/Argentina/La_Rioja" + | "America/Argentina/Mendoza" + | "America/Argentina/Rio_Gallegos" + | "America/Argentina/Salta" + | "America/Argentina/San_Juan" + | "America/Argentina/San_Luis" + | "America/Argentina/Tucuman" + | "America/Argentina/Ushuaia" + | "America/Aruba" + | "America/Asuncion" + | "America/Atikokan" + | "America/Atka" + | "America/Bahia" + | "America/Bahia_Banderas" + | "America/Barbados" + | "America/Belem" + | "America/Belize" + | "America/Blanc-Sablon" + | "America/Boa_Vista" + | "America/Bogota" + | "America/Boise" + | "America/Buenos_Aires" + | "America/Cambridge_Bay" + | "America/Campo_Grande" + | "America/Cancun" + | "America/Caracas" + | "America/Catamarca" + | "America/Cayenne" + | "America/Cayman" + | "America/Chicago" + | "America/Chihuahua" + | "America/Coral_Harbour" + | "America/Cordoba" + | "America/Costa_Rica" + | "America/Creston" + | "America/Cuiaba" + | "America/Curacao" + | "America/Danmarkshavn" + | "America/Dawson" + | "America/Dawson_Creek" + | "America/Denver" + | "America/Detroit" + | "America/Dominica" + | "America/Edmonton" + | "America/Eirunepe" + | "America/El_Salvador" + | "America/Ensenada" + | "America/Fort_Nelson" + | "America/Fort_Wayne" + | "America/Fortaleza" + | "America/Glace_Bay" + | "America/Godthab" + | "America/Goose_Bay" + | "America/Grand_Turk" + | "America/Grenada" + | "America/Guadeloupe" + | "America/Guatemala" + | "America/Guayaquil" + | "America/Guyana" + | "America/Halifax" + | "America/Havana" + | "America/Hermosillo" + | "America/Indiana/Indianapolis" + | "America/Indiana/Knox" + | "America/Indiana/Marengo" + | "America/Indiana/Petersburg" + | "America/Indiana/Tell_City" + | "America/Indiana/Vevay" + | "America/Indiana/Vincennes" + | "America/Indiana/Winamac" + | "America/Indianapolis" + | "America/Inuvik" + | "America/Iqaluit" + | "America/Jamaica" + | "America/Jujuy" + | "America/Juneau" + | "America/Kentucky/Louisville" + | "America/Kentucky/Monticello" + | "America/Knox_IN" + | "America/Kralendijk" + | "America/La_Paz" + | "America/Lima" + | "America/Los_Angeles" + | "America/Louisville" + | "America/Lower_Princes" + | "America/Maceio" + | "America/Managua" + | "America/Manaus" + | "America/Marigot" + | "America/Martinique" + | "America/Matamoros" + | "America/Mazatlan" + | "America/Mendoza" + | "America/Menominee" + | "America/Merida" + | "America/Metlakatla" + | "America/Mexico_City" + | "America/Miquelon" + | "America/Moncton" + | "America/Monterrey" + | "America/Montevideo" + | "America/Montreal" + | "America/Montserrat" + | "America/Nassau" + | "America/New_York" + | "America/Nipigon" + | "America/Nome" + | "America/Noronha" + | "America/North_Dakota/Beulah" + | "America/North_Dakota/Center" + | "America/North_Dakota/New_Salem" + | "America/Ojinaga" + | "America/Panama" + | "America/Pangnirtung" + | "America/Paramaribo" + | "America/Phoenix" + | "America/Port-au-Prince" + | "America/Port_of_Spain" + | "America/Porto_Acre" + | "America/Porto_Velho" + | "America/Puerto_Rico" + | "America/Punta_Arenas" + | "America/Rainy_River" + | "America/Rankin_Inlet" + | "America/Recife" + | "America/Regina" + | "America/Resolute" + | "America/Rio_Branco" + | "America/Rosario" + | "America/Santa_Isabel" + | "America/Santarem" + | "America/Santiago" + | "America/Santo_Domingo" + | "America/Sao_Paulo" + | "America/Scoresbysund" + | "America/Shiprock" + | "America/Sitka" + | "America/St_Barthelemy" + | "America/St_Johns" + | "America/St_Kitts" + | "America/St_Lucia" + | "America/St_Thomas" + | "America/St_Vincent" + | "America/Swift_Current" + | "America/Tegucigalpa" + | "America/Thule" + | "America/Thunder_Bay" + | "America/Tijuana" + | "America/Toronto" + | "America/Tortola" + | "America/Vancouver" + | "America/Virgin" + | "America/Whitehorse" + | "America/Winnipeg" + | "America/Yakutat" + | "America/Yellowknife" + | "Antarctica/Casey" + | "Antarctica/Davis" + | "Antarctica/DumontDUrville" + | "Antarctica/Macquarie" + | "Antarctica/Mawson" + | "Antarctica/McMurdo" + | "Antarctica/Palmer" + | "Antarctica/Rothera" + | "Antarctica/South_Pole" + | "Antarctica/Syowa" + | "Antarctica/Troll" + | "Antarctica/Vostok" + | "Arctic/Longyearbyen" + | "Asia/Aden" + | "Asia/Almaty" + | "Asia/Amman" + | "Asia/Anadyr" + | "Asia/Aqtau" + | "Asia/Aqtobe" + | "Asia/Ashgabat" + | "Asia/Ashkhabad" + | "Asia/Atyrau" + | "Asia/Baghdad" + | "Asia/Bahrain" + | "Asia/Baku" + | "Asia/Bangkok" + | "Asia/Barnaul" + | "Asia/Beirut" + | "Asia/Bishkek" + | "Asia/Brunei" + | "Asia/Calcutta" + | "Asia/Chita" + | "Asia/Choibalsan" + | "Asia/Chongqing" + | "Asia/Chungking" + | "Asia/Colombo" + | "Asia/Dacca" + | "Asia/Damascus" + | "Asia/Dhaka" + | "Asia/Dili" + | "Asia/Dubai" + | "Asia/Dushanbe" + | "Asia/Famagusta" + | "Asia/Gaza" + | "Asia/Harbin" + | "Asia/Hebron" + | "Asia/Ho_Chi_Minh" + | "Asia/Hong_Kong" + | "Asia/Hovd" + | "Asia/Irkutsk" + | "Asia/Istanbul" + | "Asia/Jakarta" + | "Asia/Jayapura" + | "Asia/Jerusalem" + | "Asia/Kabul" + | "Asia/Kamchatka" + | "Asia/Karachi" + | "Asia/Kashgar" + | "Asia/Kathmandu" + | "Asia/Katmandu" + | "Asia/Khandyga" + | "Asia/Kolkata" + | "Asia/Krasnoyarsk" + | "Asia/Kuala_Lumpur" + | "Asia/Kuching" + | "Asia/Kuwait" + | "Asia/Macao" + | "Asia/Macau" + | "Asia/Magadan" + | "Asia/Makassar" + | "Asia/Manila" + | "Asia/Muscat" + | "Asia/Nicosia" + | "Asia/Novokuznetsk" + | "Asia/Novosibirsk" + | "Asia/Omsk" + | "Asia/Oral" + | "Asia/Phnom_Penh" + | "Asia/Pontianak" + | "Asia/Pyongyang" + | "Asia/Qatar" + | "Asia/Qostanay" + | "Asia/Qyzylorda" + | "Asia/Rangoon" + | "Asia/Riyadh" + | "Asia/Saigon" + | "Asia/Sakhalin" + | "Asia/Samarkand" + | "Asia/Seoul" + | "Asia/Shanghai" + | "Asia/Singapore" + | "Asia/Srednekolymsk" + | "Asia/Taipei" + | "Asia/Tashkent" + | "Asia/Tbilisi" + | "Asia/Tehran" + | "Asia/Tel_Aviv" + | "Asia/Thimbu" + | "Asia/Thimphu" + | "Asia/Tokyo" + | "Asia/Tomsk" + | "Asia/Ujung_Pandang" + | "Asia/Ulaanbaatar" + | "Asia/Ulan_Bator" + | "Asia/Urumqi" + | "Asia/Ust-Nera" + | "Asia/Vientiane" + | "Asia/Vladivostok" + | "Asia/Yakutsk" + | "Asia/Yangon" + | "Asia/Yekaterinburg" + | "Asia/Yerevan" + | "Atlantic/Azores" + | "Atlantic/Bermuda" + | "Atlantic/Canary" + | "Atlantic/Cape_Verde" + | "Atlantic/Faeroe" + | "Atlantic/Faroe" + | "Atlantic/Jan_Mayen" + | "Atlantic/Madeira" + | "Atlantic/Reykjavik" + | "Atlantic/South_Georgia" + | "Atlantic/St_Helena" + | "Atlantic/Stanley" + | "Australia/ACT" + | "Australia/Adelaide" + | "Australia/Brisbane" + | "Australia/Broken_Hill" + | "Australia/Canberra" + | "Australia/Currie" + | "Australia/Darwin" + | "Australia/Eucla" + | "Australia/Hobart" + | "Australia/LHI" + | "Australia/Lindeman" + | "Australia/Lord_Howe" + | "Australia/Melbourne" + | "Australia/NSW" + | "Australia/North" + | "Australia/Perth" + | "Australia/Queensland" + | "Australia/South" + | "Australia/Sydney" + | "Australia/Tasmania" + | "Australia/Victoria" + | "Australia/West" + | "Australia/Yancowinna" + | "Brazil/Acre" + | "Brazil/DeNoronha" + | "Brazil/East" + | "Brazil/West" + | "CET" + | "CST6CDT" + | "Canada/Atlantic" + | "Canada/Central" + | "Canada/Eastern" + | "Canada/Mountain" + | "Canada/Newfoundland" + | "Canada/Pacific" + | "Canada/Saskatchewan" + | "Canada/Yukon" + | "Chile/Continental" + | "Chile/EasterIsland" + | "Cuba" + | "EET" + | "EST" + | "EST5EDT" + | "Egypt" + | "Eire" + | "Etc/GMT" + | "Etc/GMT+0" + | "Etc/GMT+1" + | "Etc/GMT+10" + | "Etc/GMT+11" + | "Etc/GMT+12" + | "Etc/GMT+2" + | "Etc/GMT+3" + | "Etc/GMT+4" + | "Etc/GMT+5" + | "Etc/GMT+6" + | "Etc/GMT+7" + | "Etc/GMT+8" + | "Etc/GMT+9" + | "Etc/GMT-0" + | "Etc/GMT-1" + | "Etc/GMT-10" + | "Etc/GMT-11" + | "Etc/GMT-12" + | "Etc/GMT-13" + | "Etc/GMT-14" + | "Etc/GMT-2" + | "Etc/GMT-3" + | "Etc/GMT-4" + | "Etc/GMT-5" + | "Etc/GMT-6" + | "Etc/GMT-7" + | "Etc/GMT-8" + | "Etc/GMT-9" + | "Etc/GMT0" + | "Etc/Greenwich" + | "Etc/UCT" + | "Etc/UTC" + | "Etc/Universal" + | "Etc/Zulu" + | "Europe/Amsterdam" + | "Europe/Andorra" + | "Europe/Astrakhan" + | "Europe/Athens" + | "Europe/Belfast" + | "Europe/Belgrade" + | "Europe/Berlin" + | "Europe/Bratislava" + | "Europe/Brussels" + | "Europe/Bucharest" + | "Europe/Budapest" + | "Europe/Busingen" + | "Europe/Chisinau" + | "Europe/Copenhagen" + | "Europe/Dublin" + | "Europe/Gibraltar" + | "Europe/Guernsey" + | "Europe/Helsinki" + | "Europe/Isle_of_Man" + | "Europe/Istanbul" + | "Europe/Jersey" + | "Europe/Kaliningrad" + | "Europe/Kiev" + | "Europe/Kirov" + | "Europe/Lisbon" + | "Europe/Ljubljana" + | "Europe/London" + | "Europe/Luxembourg" + | "Europe/Madrid" + | "Europe/Malta" + | "Europe/Mariehamn" + | "Europe/Minsk" + | "Europe/Monaco" + | "Europe/Moscow" + | "Europe/Nicosia" + | "Europe/Oslo" + | "Europe/Paris" + | "Europe/Podgorica" + | "Europe/Prague" + | "Europe/Riga" + | "Europe/Rome" + | "Europe/Samara" + | "Europe/San_Marino" + | "Europe/Sarajevo" + | "Europe/Saratov" + | "Europe/Simferopol" + | "Europe/Skopje" + | "Europe/Sofia" + | "Europe/Stockholm" + | "Europe/Tallinn" + | "Europe/Tirane" + | "Europe/Tiraspol" + | "Europe/Ulyanovsk" + | "Europe/Uzhgorod" + | "Europe/Vaduz" + | "Europe/Vatican" + | "Europe/Vienna" + | "Europe/Vilnius" + | "Europe/Volgograd" + | "Europe/Warsaw" + | "Europe/Zagreb" + | "Europe/Zaporozhye" + | "Europe/Zurich" + | "GB" + | "GB-Eire" + | "GMT" + | "GMT+0" + | "GMT-0" + | "GMT0" + | "Greenwich" + | "HST" + | "Hongkong" + | "Iceland" + | "Indian/Antananarivo" + | "Indian/Chagos" + | "Indian/Christmas" + | "Indian/Cocos" + | "Indian/Comoro" + | "Indian/Kerguelen" + | "Indian/Mahe" + | "Indian/Maldives" + | "Indian/Mauritius" + | "Indian/Mayotte" + | "Indian/Reunion" + | "Iran" + | "Israel" + | "Jamaica" + | "Japan" + | "Kwajalein" + | "Libya" + | "MET" + | "MST" + | "MST7MDT" + | "Mexico/BajaNorte" + | "Mexico/BajaSur" + | "Mexico/General" + | "NZ" + | "NZ-CHAT" + | "Navajo" + | "PRC" + | "PST8PDT" + | "Pacific/Apia" + | "Pacific/Auckland" + | "Pacific/Bougainville" + | "Pacific/Chatham" + | "Pacific/Chuuk" + | "Pacific/Easter" + | "Pacific/Efate" + | "Pacific/Enderbury" + | "Pacific/Fakaofo" + | "Pacific/Fiji" + | "Pacific/Funafuti" + | "Pacific/Galapagos" + | "Pacific/Gambier" + | "Pacific/Guadalcanal" + | "Pacific/Guam" + | "Pacific/Honolulu" + | "Pacific/Johnston" + | "Pacific/Kiritimati" + | "Pacific/Kosrae" + | "Pacific/Kwajalein" + | "Pacific/Majuro" + | "Pacific/Marquesas" + | "Pacific/Midway" + | "Pacific/Nauru" + | "Pacific/Niue" + | "Pacific/Norfolk" + | "Pacific/Noumea" + | "Pacific/Pago_Pago" + | "Pacific/Palau" + | "Pacific/Pitcairn" + | "Pacific/Pohnpei" + | "Pacific/Ponape" + | "Pacific/Port_Moresby" + | "Pacific/Rarotonga" + | "Pacific/Saipan" + | "Pacific/Samoa" + | "Pacific/Tahiti" + | "Pacific/Tarawa" + | "Pacific/Tongatapu" + | "Pacific/Truk" + | "Pacific/Wake" + | "Pacific/Wallis" + | "Pacific/Yap" + | "Poland" + | "Portugal" + | "ROC" + | "ROK" + | "Singapore" + | "Turkey" + | "UCT" + | "US/Alaska" + | "US/Aleutian" + | "US/Arizona" + | "US/Central" + | "US/East-Indiana" + | "US/Eastern" + | "US/Hawaii" + | "US/Indiana-Starke" + | "US/Michigan" + | "US/Mountain" + | "US/Pacific" + | "US/Pacific-New" + | "US/Samoa" + | "UTC" + | "Universal" + | "W-SU" + | "WET" + | "Zulu" + +type DateResponse = { + start: string + end: string | null + time_zone: TimeZoneRequest | null +} + +type TextRequest = string + +type StringFormulaPropertyResponse = { type: "string"; string: string | null } + +type DateFormulaPropertyResponse = { type: "date"; date: DateResponse | null } + +type NumberFormulaPropertyResponse = { type: "number"; number: number | null } + +type BooleanFormulaPropertyResponse = { + type: "boolean" + boolean: boolean | null +} + +type FormulaPropertyResponse = + | StringFormulaPropertyResponse + | DateFormulaPropertyResponse + | NumberFormulaPropertyResponse + | BooleanFormulaPropertyResponse + +type VerificationPropertyUnverifiedResponse = { + state: "unverified" + date: null + verified_by: null +} + +type VerificationPropertyResponse = { + state: "verified" | "expired" + date: DateResponse | null + verified_by: + | { id: IdRequest } + | null + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | null + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | null +} + +type AnnotationResponse = { + bold: boolean + italic: boolean + strikethrough: boolean + underline: boolean + code: boolean + color: + | "default" + | "gray" + | "brown" + | "orange" + | "yellow" + | "green" + | "blue" + | "purple" + | "pink" + | "red" + | "gray_background" + | "brown_background" + | "orange_background" + | "yellow_background" + | "green_background" + | "blue_background" + | "purple_background" + | "pink_background" + | "red_background" +} + +export type TextRichTextItemResponse = { + type: "text" + text: { content: string; link: { url: TextRequest } | null } + annotations: AnnotationResponse + plain_text: string + href: string | null +} + +type LinkPreviewMentionResponse = { url: TextRequest } + +type TemplateMentionDateTemplateMentionResponse = { + type: "template_mention_date" + template_mention_date: "today" | "now" +} + +type TemplateMentionUserTemplateMentionResponse = { + type: "template_mention_user" + template_mention_user: "me" +} + +type TemplateMentionResponse = + | TemplateMentionDateTemplateMentionResponse + | TemplateMentionUserTemplateMentionResponse + +export type MentionRichTextItemResponse = { + type: "mention" + mention: + | { type: "user"; user: PartialUserObjectResponse | UserObjectResponse } + | { type: "date"; date: DateResponse } + | { type: "link_preview"; link_preview: LinkPreviewMentionResponse } + | { type: "template_mention"; template_mention: TemplateMentionResponse } + | { type: "page"; page: { id: IdRequest } } + | { type: "database"; database: { id: IdRequest } } + annotations: AnnotationResponse + plain_text: string + href: string | null +} + +export type EquationRichTextItemResponse = { + type: "equation" + equation: { expression: TextRequest } + annotations: AnnotationResponse + plain_text: string + href: string | null +} + +export type RichTextItemResponse = + | TextRichTextItemResponse + | MentionRichTextItemResponse + | EquationRichTextItemResponse + +type RollupFunction = + | "count" + | "count_values" + | "empty" + | "not_empty" + | "unique" + | "show_unique" + | "percent_empty" + | "percent_not_empty" + | "sum" + | "average" + | "median" + | "min" + | "max" + | "range" + | "earliest_date" + | "latest_date" + | "date_range" + | "checked" + | "unchecked" + | "percent_checked" + | "percent_unchecked" + | "count_per_group" + | "percent_per_group" + | "show_original" + +type EmojiRequest = + | "😀" + | "😃" + | "😄" + | "😁" + | "😆" + | "😅" + | "🤣" + | "😂" + | "🙂" + | "🙃" + | "😉" + | "😊" + | "😇" + | "🥰" + | "😍" + | "🤩" + | "😘" + | "😗" + | "☺️" + | "☺" + | "😚" + | "😙" + | "🥲" + | "😋" + | "😛" + | "😜" + | "🤪" + | "😝" + | "🤑" + | "🤗" + | "🤭" + | "🤫" + | "🤔" + | "🤐" + | "🤨" + | "😐" + | "😑" + | "😶" + | "😶‍🌫️" + | "😶‍🌫" + | "😏" + | "😒" + | "🙄" + | "😬" + | "😮‍💨" + | "🤥" + | "😌" + | "😔" + | "😪" + | "🤤" + | "😴" + | "😷" + | "🤒" + | "🤕" + | "🤢" + | "🤮" + | "🤧" + | "🥵" + | "🥶" + | "🥴" + | "😵" + | "😵‍💫" + | "🤯" + | "🤠" + | "🥳" + | "🥸" + | "😎" + | "🤓" + | "🧐" + | "😕" + | "😟" + | "🙁" + | "☹️" + | "☹" + | "😮" + | "😯" + | "😲" + | "😳" + | "🥺" + | "😦" + | "😧" + | "😨" + | "😰" + | "😥" + | "😢" + | "😭" + | "😱" + | "😖" + | "😣" + | "😞" + | "😓" + | "😩" + | "😫" + | "🥱" + | "😤" + | "😡" + | "😠" + | "🤬" + | "😈" + | "👿" + | "💀" + | "☠️" + | "☠" + | "💩" + | "🤡" + | "👹" + | "👺" + | "👻" + | "👽" + | "👾" + | "🤖" + | "😺" + | "😸" + | "😹" + | "😻" + | "😼" + | "😽" + | "🙀" + | "😿" + | "😾" + | "🙈" + | "🙉" + | "🙊" + | "💋" + | "💌" + | "💘" + | "💝" + | "💖" + | "💗" + | "💓" + | "💞" + | "💕" + | "💟" + | "❣️" + | "❣" + | "💔" + | "❤️‍🔥" + | "❤‍🔥" + | "❤️‍🩹" + | "❤‍🩹" + | "❤️" + | "❤" + | "🧡" + | "💛" + | "💚" + | "💙" + | "💜" + | "🤎" + | "🖤" + | "🤍" + | "💯" + | "💢" + | "💥" + | "💫" + | "💦" + | "💨" + | "🕳️" + | "🕳" + | "💣" + | "💬" + | "👁️‍🗨️" + | "🗨️" + | "🗨" + | "🗯️" + | "🗯" + | "💭" + | "💤" + | "👋🏻" + | "👋🏼" + | "👋🏽" + | "👋🏾" + | "👋🏿" + | "👋" + | "🤚🏻" + | "🤚🏼" + | "🤚🏽" + | "🤚🏾" + | "🤚🏿" + | "🤚" + | "🖐🏻" + | "🖐🏼" + | "🖐🏽" + | "🖐🏾" + | "🖐🏿" + | "🖐️" + | "🖐" + | "✋🏻" + | "✋🏼" + | "✋🏽" + | "✋🏾" + | "✋🏿" + | "✋" + | "🖖🏻" + | "🖖🏼" + | "🖖🏽" + | "🖖🏾" + | "🖖🏿" + | "🖖" + | "👌🏻" + | "👌🏼" + | "👌🏽" + | "👌🏾" + | "👌🏿" + | "👌" + | "🤌🏻" + | "🤌🏼" + | "🤌🏽" + | "🤌🏾" + | "🤌🏿" + | "🤌" + | "🤏🏻" + | "🤏🏼" + | "🤏🏽" + | "🤏🏾" + | "🤏🏿" + | "🤏" + | "✌🏻" + | "✌🏼" + | "✌🏽" + | "✌🏾" + | "✌🏿" + | "✌️" + | "✌" + | "🤞🏻" + | "🤞🏼" + | "🤞🏽" + | "🤞🏾" + | "🤞🏿" + | "🤞" + | "🤟🏻" + | "🤟🏼" + | "🤟🏽" + | "🤟🏾" + | "🤟🏿" + | "🤟" + | "🤘🏻" + | "🤘🏼" + | "🤘🏽" + | "🤘🏾" + | "🤘🏿" + | "🤘" + | "🤙🏻" + | "🤙🏼" + | "🤙🏽" + | "🤙🏾" + | "🤙🏿" + | "🤙" + | "👈🏻" + | "👈🏼" + | "👈🏽" + | "👈🏾" + | "👈🏿" + | "👈" + | "👉🏻" + | "👉🏼" + | "👉🏽" + | "👉🏾" + | "👉🏿" + | "👉" + | "👆🏻" + | "👆🏼" + | "👆🏽" + | "👆🏾" + | "👆🏿" + | "👆" + | "🖕🏻" + | "🖕🏼" + | "🖕🏽" + | "🖕🏾" + | "🖕🏿" + | "🖕" + | "👇🏻" + | "👇🏼" + | "👇🏽" + | "👇🏾" + | "👇🏿" + | "👇" + | "☝🏻" + | "☝🏼" + | "☝🏽" + | "☝🏾" + | "☝🏿" + | "☝️" + | "☝" + | "👍🏻" + | "👍🏼" + | "👍🏽" + | "👍🏾" + | "👍🏿" + | "👍" + | "👎🏻" + | "👎🏼" + | "👎🏽" + | "👎🏾" + | "👎🏿" + | "👎" + | "✊🏻" + | "✊🏼" + | "✊🏽" + | "✊🏾" + | "✊🏿" + | "✊" + | "👊🏻" + | "👊🏼" + | "👊🏽" + | "👊🏾" + | "👊🏿" + | "👊" + | "🤛🏻" + | "🤛🏼" + | "🤛🏽" + | "🤛🏾" + | "🤛🏿" + | "🤛" + | "🤜🏻" + | "🤜🏼" + | "🤜🏽" + | "🤜🏾" + | "🤜🏿" + | "🤜" + | "👏🏻" + | "👏🏼" + | "👏🏽" + | "👏🏾" + | "👏🏿" + | "👏" + | "🙌🏻" + | "🙌🏼" + | "🙌🏽" + | "🙌🏾" + | "🙌🏿" + | "🙌" + | "👐🏻" + | "👐🏼" + | "👐🏽" + | "👐🏾" + | "👐🏿" + | "👐" + | "🤲🏻" + | "🤲🏼" + | "🤲🏽" + | "🤲🏾" + | "🤲🏿" + | "🤲" + | "🤝" + | "🙏🏻" + | "🙏🏼" + | "🙏🏽" + | "🙏🏾" + | "🙏🏿" + | "🙏" + | "✍🏻" + | "✍🏼" + | "✍🏽" + | "✍🏾" + | "✍🏿" + | "✍️" + | "✍" + | "💅🏻" + | "💅🏼" + | "💅🏽" + | "💅🏾" + | "💅🏿" + | "💅" + | "🤳🏻" + | "🤳🏼" + | "🤳🏽" + | "🤳🏾" + | "🤳🏿" + | "🤳" + | "💪🏻" + | "💪🏼" + | "💪🏽" + | "💪🏾" + | "💪🏿" + | "💪" + | "🦾" + | "🦿" + | "🦵🏻" + | "🦵🏼" + | "🦵🏽" + | "🦵🏾" + | "🦵🏿" + | "🦵" + | "🦶🏻" + | "🦶🏼" + | "🦶🏽" + | "🦶🏾" + | "🦶🏿" + | "🦶" + | "👂🏻" + | "👂🏼" + | "👂🏽" + | "👂🏾" + | "👂🏿" + | "👂" + | "🦻🏻" + | "🦻🏼" + | "🦻🏽" + | "🦻🏾" + | "🦻🏿" + | "🦻" + | "👃🏻" + | "👃🏼" + | "👃🏽" + | "👃🏾" + | "👃🏿" + | "👃" + | "🧠" + | "🫀" + | "🫁" + | "🦷" + | "🦴" + | "👀" + | "👁️" + | "👁" + | "👅" + | "👄" + | "👶🏻" + | "👶🏼" + | "👶🏽" + | "👶🏾" + | "👶🏿" + | "👶" + | "🧒🏻" + | "🧒🏼" + | "🧒🏽" + | "🧒🏾" + | "🧒🏿" + | "🧒" + | "👦🏻" + | "👦🏼" + | "👦🏽" + | "👦🏾" + | "👦🏿" + | "👦" + | "👧🏻" + | "👧🏼" + | "👧🏽" + | "👧🏾" + | "👧🏿" + | "👧" + | "🧑🏻" + | "🧑🏼" + | "🧑🏽" + | "🧑🏾" + | "🧑🏿" + | "🧑" + | "👱🏻" + | "👱🏼" + | "👱🏽" + | "👱🏾" + | "👱🏿" + | "👱" + | "👨🏻" + | "👨🏼" + | "👨🏽" + | "👨🏾" + | "👨🏿" + | "👨" + | "🧔🏻" + | "🧔🏼" + | "🧔🏽" + | "🧔🏾" + | "🧔🏿" + | "🧔" + | "🧔🏻‍♂️" + | "🧔🏼‍♂️" + | "🧔🏽‍♂️" + | "🧔🏾‍♂️" + | "🧔🏿‍♂️" + | "🧔‍♂️" + | "🧔‍♂" + | "🧔🏻‍♀️" + | "🧔🏼‍♀️" + | "🧔🏽‍♀️" + | "🧔🏾‍♀️" + | "🧔🏿‍♀️" + | "🧔‍♀️" + | "🧔‍♀" + | "👨🏻‍🦰" + | "👨🏼‍🦰" + | "👨🏽‍🦰" + | "👨🏾‍🦰" + | "👨🏿‍🦰" + | "👨‍🦰" + | "👨🏻‍🦱" + | "👨🏼‍🦱" + | "👨🏽‍🦱" + | "👨🏾‍🦱" + | "👨🏿‍🦱" + | "👨‍🦱" + | "👨🏻‍🦳" + | "👨🏼‍🦳" + | "👨🏽‍🦳" + | "👨🏾‍🦳" + | "👨🏿‍🦳" + | "👨‍🦳" + | "👨🏻‍🦲" + | "👨🏼‍🦲" + | "👨🏽‍🦲" + | "👨🏾‍🦲" + | "👨🏿‍🦲" + | "👨‍🦲" + | "👩🏻" + | "👩🏼" + | "👩🏽" + | "👩🏾" + | "👩🏿" + | "👩" + | "👩🏻‍🦰" + | "👩🏼‍🦰" + | "👩🏽‍🦰" + | "👩🏾‍🦰" + | "👩🏿‍🦰" + | "👩‍🦰" + | "🧑🏻‍🦰" + | "🧑🏼‍🦰" + | "🧑🏽‍🦰" + | "🧑🏾‍🦰" + | "🧑🏿‍🦰" + | "🧑‍🦰" + | "👩🏻‍🦱" + | "👩🏼‍🦱" + | "👩🏽‍🦱" + | "👩🏾‍🦱" + | "👩🏿‍🦱" + | "👩‍🦱" + | "🧑🏻‍🦱" + | "🧑🏼‍🦱" + | "🧑🏽‍🦱" + | "🧑🏾‍🦱" + | "🧑🏿‍🦱" + | "🧑‍🦱" + | "👩🏻‍🦳" + | "👩🏼‍🦳" + | "👩🏽‍🦳" + | "👩🏾‍🦳" + | "👩🏿‍🦳" + | "👩‍🦳" + | "🧑🏻‍🦳" + | "🧑🏼‍🦳" + | "🧑🏽‍🦳" + | "🧑🏾‍🦳" + | "🧑🏿‍🦳" + | "🧑‍🦳" + | "👩🏻‍🦲" + | "👩🏼‍🦲" + | "👩🏽‍🦲" + | "👩🏾‍🦲" + | "👩🏿‍🦲" + | "👩‍🦲" + | "🧑🏻‍🦲" + | "🧑🏼‍🦲" + | "🧑🏽‍🦲" + | "🧑🏾‍🦲" + | "🧑🏿‍🦲" + | "🧑‍🦲" + | "👱🏻‍♀️" + | "👱🏼‍♀️" + | "👱🏽‍♀️" + | "👱🏾‍♀️" + | "👱🏿‍♀️" + | "👱‍♀️" + | "👱‍♀" + | "👱🏻‍♂️" + | "👱🏼‍♂️" + | "👱🏽‍♂️" + | "👱🏾‍♂️" + | "👱🏿‍♂️" + | "👱‍♂️" + | "👱‍♂" + | "🧓🏻" + | "🧓🏼" + | "🧓🏽" + | "🧓🏾" + | "🧓🏿" + | "🧓" + | "👴🏻" + | "👴🏼" + | "👴🏽" + | "👴🏾" + | "👴🏿" + | "👴" + | "👵🏻" + | "👵🏼" + | "👵🏽" + | "👵🏾" + | "👵🏿" + | "👵" + | "🙍🏻" + | "🙍🏼" + | "🙍🏽" + | "🙍🏾" + | "🙍🏿" + | "🙍" + | "🙍🏻‍♂️" + | "🙍🏼‍♂️" + | "🙍🏽‍♂️" + | "🙍🏾‍♂️" + | "🙍🏿‍♂️" + | "🙍‍♂️" + | "🙍‍♂" + | "🙍🏻‍♀️" + | "🙍🏼‍♀️" + | "🙍🏽‍♀️" + | "🙍🏾‍♀️" + | "🙍🏿‍♀️" + | "🙍‍♀️" + | "🙍‍♀" + | "🙎🏻" + | "🙎🏼" + | "🙎🏽" + | "🙎🏾" + | "🙎🏿" + | "🙎" + | "🙎🏻‍♂️" + | "🙎🏼‍♂️" + | "🙎🏽‍♂️" + | "🙎🏾‍♂️" + | "🙎🏿‍♂️" + | "🙎‍♂️" + | "🙎‍♂" + | "🙎🏻‍♀️" + | "🙎🏼‍♀️" + | "🙎🏽‍♀️" + | "🙎🏾‍♀️" + | "🙎🏿‍♀️" + | "🙎‍♀️" + | "🙎‍♀" + | "🙅🏻" + | "🙅🏼" + | "🙅🏽" + | "🙅🏾" + | "🙅🏿" + | "🙅" + | "🙅🏻‍♂️" + | "🙅🏼‍♂️" + | "🙅🏽‍♂️" + | "🙅🏾‍♂️" + | "🙅🏿‍♂️" + | "🙅‍♂️" + | "🙅‍♂" + | "🙅🏻‍♀️" + | "🙅🏼‍♀️" + | "🙅🏽‍♀️" + | "🙅🏾‍♀️" + | "🙅🏿‍♀️" + | "🙅‍♀️" + | "🙅‍♀" + | "🙆🏻" + | "🙆🏼" + | "🙆🏽" + | "🙆🏾" + | "🙆🏿" + | "🙆" + | "🙆🏻‍♂️" + | "🙆🏼‍♂️" + | "🙆🏽‍♂️" + | "🙆🏾‍♂️" + | "🙆🏿‍♂️" + | "🙆‍♂️" + | "🙆‍♂" + | "🙆🏻‍♀️" + | "🙆🏼‍♀️" + | "🙆🏽‍♀️" + | "🙆🏾‍♀️" + | "🙆🏿‍♀️" + | "🙆‍♀️" + | "🙆‍♀" + | "💁🏻" + | "💁🏼" + | "💁🏽" + | "💁🏾" + | "💁🏿" + | "💁" + | "💁🏻‍♂️" + | "💁🏼‍♂️" + | "💁🏽‍♂️" + | "💁🏾‍♂️" + | "💁🏿‍♂️" + | "💁‍♂️" + | "💁‍♂" + | "💁🏻‍♀️" + | "💁🏼‍♀️" + | "💁🏽‍♀️" + | "💁🏾‍♀️" + | "💁🏿‍♀️" + | "💁‍♀️" + | "💁‍♀" + | "🙋🏻" + | "🙋🏼" + | "🙋🏽" + | "🙋🏾" + | "🙋🏿" + | "🙋" + | "🙋🏻‍♂️" + | "🙋🏼‍♂️" + | "🙋🏽‍♂️" + | "🙋🏾‍♂️" + | "🙋🏿‍♂️" + | "🙋‍♂️" + | "🙋‍♂" + | "🙋🏻‍♀️" + | "🙋🏼‍♀️" + | "🙋🏽‍♀️" + | "🙋🏾‍♀️" + | "🙋🏿‍♀️" + | "🙋‍♀️" + | "🙋‍♀" + | "🧏🏻" + | "🧏🏼" + | "🧏🏽" + | "🧏🏾" + | "🧏🏿" + | "🧏" + | "🧏🏻‍♂️" + | "🧏🏼‍♂️" + | "🧏🏽‍♂️" + | "🧏🏾‍♂️" + | "🧏🏿‍♂️" + | "🧏‍♂️" + | "🧏‍♂" + | "🧏🏻‍♀️" + | "🧏🏼‍♀️" + | "🧏🏽‍♀️" + | "🧏🏾‍♀️" + | "🧏🏿‍♀️" + | "🧏‍♀️" + | "🧏‍♀" + | "🙇🏻" + | "🙇🏼" + | "🙇🏽" + | "🙇🏾" + | "🙇🏿" + | "🙇" + | "🙇🏻‍♂️" + | "🙇🏼‍♂️" + | "🙇🏽‍♂️" + | "🙇🏾‍♂️" + | "🙇🏿‍♂️" + | "🙇‍♂️" + | "🙇‍♂" + | "🙇🏻‍♀️" + | "🙇🏼‍♀️" + | "🙇🏽‍♀️" + | "🙇🏾‍♀️" + | "🙇🏿‍♀️" + | "🙇‍♀️" + | "🙇‍♀" + | "🤦🏻" + | "🤦🏼" + | "🤦🏽" + | "🤦🏾" + | "🤦🏿" + | "🤦" + | "🤦🏻‍♂️" + | "🤦🏼‍♂️" + | "🤦🏽‍♂️" + | "🤦🏾‍♂️" + | "🤦🏿‍♂️" + | "🤦‍♂️" + | "🤦‍♂" + | "🤦🏻‍♀️" + | "🤦🏼‍♀️" + | "🤦🏽‍♀️" + | "🤦🏾‍♀️" + | "🤦🏿‍♀️" + | "🤦‍♀️" + | "🤦‍♀" + | "🤷🏻" + | "🤷🏼" + | "🤷🏽" + | "🤷🏾" + | "🤷🏿" + | "🤷" + | "🤷🏻‍♂️" + | "🤷🏼‍♂️" + | "🤷🏽‍♂️" + | "🤷🏾‍♂️" + | "🤷🏿‍♂️" + | "🤷‍♂️" + | "🤷‍♂" + | "🤷🏻‍♀️" + | "🤷🏼‍♀️" + | "🤷🏽‍♀️" + | "🤷🏾‍♀️" + | "🤷🏿‍♀️" + | "🤷‍♀️" + | "🤷‍♀" + | "🧑🏻‍⚕️" + | "🧑🏼‍⚕️" + | "🧑🏽‍⚕️" + | "🧑🏾‍⚕️" + | "🧑🏿‍⚕️" + | "🧑‍⚕️" + | "🧑‍⚕" + | "👨🏻‍⚕️" + | "👨🏼‍⚕️" + | "👨🏽‍⚕️" + | "👨🏾‍⚕️" + | "👨🏿‍⚕️" + | "👨‍⚕️" + | "👨‍⚕" + | "👩🏻‍⚕️" + | "👩🏼‍⚕️" + | "👩🏽‍⚕️" + | "👩🏾‍⚕️" + | "👩🏿‍⚕️" + | "👩‍⚕️" + | "👩‍⚕" + | "🧑🏻‍🎓" + | "🧑🏼‍🎓" + | "🧑🏽‍🎓" + | "🧑🏾‍🎓" + | "🧑🏿‍🎓" + | "🧑‍🎓" + | "👨🏻‍🎓" + | "👨🏼‍🎓" + | "👨🏽‍🎓" + | "👨🏾‍🎓" + | "👨🏿‍🎓" + | "👨‍🎓" + | "👩🏻‍🎓" + | "👩🏼‍🎓" + | "👩🏽‍🎓" + | "👩🏾‍🎓" + | "👩🏿‍🎓" + | "👩‍🎓" + | "🧑🏻‍🏫" + | "🧑🏼‍🏫" + | "🧑🏽‍🏫" + | "🧑🏾‍🏫" + | "🧑🏿‍🏫" + | "🧑‍🏫" + | "👨🏻‍🏫" + | "👨🏼‍🏫" + | "👨🏽‍🏫" + | "👨🏾‍🏫" + | "👨🏿‍🏫" + | "👨‍🏫" + | "👩🏻‍🏫" + | "👩🏼‍🏫" + | "👩🏽‍🏫" + | "👩🏾‍🏫" + | "👩🏿‍🏫" + | "👩‍🏫" + | "🧑🏻‍⚖️" + | "🧑🏼‍⚖️" + | "🧑🏽‍⚖️" + | "🧑🏾‍⚖️" + | "🧑🏿‍⚖️" + | "🧑‍⚖️" + | "🧑‍⚖" + | "👨🏻‍⚖️" + | "👨🏼‍⚖️" + | "👨🏽‍⚖️" + | "👨🏾‍⚖️" + | "👨🏿‍⚖️" + | "👨‍⚖️" + | "👨‍⚖" + | "👩🏻‍⚖️" + | "👩🏼‍⚖️" + | "👩🏽‍⚖️" + | "👩🏾‍⚖️" + | "👩🏿‍⚖️" + | "👩‍⚖️" + | "👩‍⚖" + | "🧑🏻‍🌾" + | "🧑🏼‍🌾" + | "🧑🏽‍🌾" + | "🧑🏾‍🌾" + | "🧑🏿‍🌾" + | "🧑‍🌾" + | "👨🏻‍🌾" + | "👨🏼‍🌾" + | "👨🏽‍🌾" + | "👨🏾‍🌾" + | "👨🏿‍🌾" + | "👨‍🌾" + | "👩🏻‍🌾" + | "👩🏼‍🌾" + | "👩🏽‍🌾" + | "👩🏾‍🌾" + | "👩🏿‍🌾" + | "👩‍🌾" + | "🧑🏻‍🍳" + | "🧑🏼‍🍳" + | "🧑🏽‍🍳" + | "🧑🏾‍🍳" + | "🧑🏿‍🍳" + | "🧑‍🍳" + | "👨🏻‍🍳" + | "👨🏼‍🍳" + | "👨🏽‍🍳" + | "👨🏾‍🍳" + | "👨🏿‍🍳" + | "👨‍🍳" + | "👩🏻‍🍳" + | "👩🏼‍🍳" + | "👩🏽‍🍳" + | "👩🏾‍🍳" + | "👩🏿‍🍳" + | "👩‍🍳" + | "🧑🏻‍🔧" + | "🧑🏼‍🔧" + | "🧑🏽‍🔧" + | "🧑🏾‍🔧" + | "🧑🏿‍🔧" + | "🧑‍🔧" + | "👨🏻‍🔧" + | "👨🏼‍🔧" + | "👨🏽‍🔧" + | "👨🏾‍🔧" + | "👨🏿‍🔧" + | "👨‍🔧" + | "👩🏻‍🔧" + | "👩🏼‍🔧" + | "👩🏽‍🔧" + | "👩🏾‍🔧" + | "👩🏿‍🔧" + | "👩‍🔧" + | "🧑🏻‍🏭" + | "🧑🏼‍🏭" + | "🧑🏽‍🏭" + | "🧑🏾‍🏭" + | "🧑🏿‍🏭" + | "🧑‍🏭" + | "👨🏻‍🏭" + | "👨🏼‍🏭" + | "👨🏽‍🏭" + | "👨🏾‍🏭" + | "👨🏿‍🏭" + | "👨‍🏭" + | "👩🏻‍🏭" + | "👩🏼‍🏭" + | "👩🏽‍🏭" + | "👩🏾‍🏭" + | "👩🏿‍🏭" + | "👩‍🏭" + | "🧑🏻‍💼" + | "🧑🏼‍💼" + | "🧑🏽‍💼" + | "🧑🏾‍💼" + | "🧑🏿‍💼" + | "🧑‍💼" + | "👨🏻‍💼" + | "👨🏼‍💼" + | "👨🏽‍💼" + | "👨🏾‍💼" + | "👨🏿‍💼" + | "👨‍💼" + | "👩🏻‍💼" + | "👩🏼‍💼" + | "👩🏽‍💼" + | "👩🏾‍💼" + | "👩🏿‍💼" + | "👩‍💼" + | "🧑🏻‍🔬" + | "🧑🏼‍🔬" + | "🧑🏽‍🔬" + | "🧑🏾‍🔬" + | "🧑🏿‍🔬" + | "🧑‍🔬" + | "👨🏻‍🔬" + | "👨🏼‍🔬" + | "👨🏽‍🔬" + | "👨🏾‍🔬" + | "👨🏿‍🔬" + | "👨‍🔬" + | "👩🏻‍🔬" + | "👩🏼‍🔬" + | "👩🏽‍🔬" + | "👩🏾‍🔬" + | "👩🏿‍🔬" + | "👩‍🔬" + | "🧑🏻‍💻" + | "🧑🏼‍💻" + | "🧑🏽‍💻" + | "🧑🏾‍💻" + | "🧑🏿‍💻" + | "🧑‍💻" + | "👨🏻‍💻" + | "👨🏼‍💻" + | "👨🏽‍💻" + | "👨🏾‍💻" + | "👨🏿‍💻" + | "👨‍💻" + | "👩🏻‍💻" + | "👩🏼‍💻" + | "👩🏽‍💻" + | "👩🏾‍💻" + | "👩🏿‍💻" + | "👩‍💻" + | "🧑🏻‍🎤" + | "🧑🏼‍🎤" + | "🧑🏽‍🎤" + | "🧑🏾‍🎤" + | "🧑🏿‍🎤" + | "🧑‍🎤" + | "👨🏻‍🎤" + | "👨🏼‍🎤" + | "👨🏽‍🎤" + | "👨🏾‍🎤" + | "👨🏿‍🎤" + | "👨‍🎤" + | "👩🏻‍🎤" + | "👩🏼‍🎤" + | "👩🏽‍🎤" + | "👩🏾‍🎤" + | "👩🏿‍🎤" + | "👩‍🎤" + | "🧑🏻‍🎨" + | "🧑🏼‍🎨" + | "🧑🏽‍🎨" + | "🧑🏾‍🎨" + | "🧑🏿‍🎨" + | "🧑‍🎨" + | "👨🏻‍🎨" + | "👨🏼‍🎨" + | "👨🏽‍🎨" + | "👨🏾‍🎨" + | "👨🏿‍🎨" + | "👨‍🎨" + | "👩🏻‍🎨" + | "👩🏼‍🎨" + | "👩🏽‍🎨" + | "👩🏾‍🎨" + | "👩🏿‍🎨" + | "👩‍🎨" + | "🧑🏻‍✈️" + | "🧑🏼‍✈️" + | "🧑🏽‍✈️" + | "🧑🏾‍✈️" + | "🧑🏿‍✈️" + | "🧑‍✈️" + | "🧑‍✈" + | "👨🏻‍✈️" + | "👨🏼‍✈️" + | "👨🏽‍✈️" + | "👨🏾‍✈️" + | "👨🏿‍✈️" + | "👨‍✈️" + | "👨‍✈" + | "👩🏻‍✈️" + | "👩🏼‍✈️" + | "👩🏽‍✈️" + | "👩🏾‍✈️" + | "👩🏿‍✈️" + | "👩‍✈️" + | "👩‍✈" + | "🧑🏻‍🚀" + | "🧑🏼‍🚀" + | "🧑🏽‍🚀" + | "🧑🏾‍🚀" + | "🧑🏿‍🚀" + | "🧑‍🚀" + | "👨🏻‍🚀" + | "👨🏼‍🚀" + | "👨🏽‍🚀" + | "👨🏾‍🚀" + | "👨🏿‍🚀" + | "👨‍🚀" + | "👩🏻‍🚀" + | "👩🏼‍🚀" + | "👩🏽‍🚀" + | "👩🏾‍🚀" + | "👩🏿‍🚀" + | "👩‍🚀" + | "🧑🏻‍🚒" + | "🧑🏼‍🚒" + | "🧑🏽‍🚒" + | "🧑🏾‍🚒" + | "🧑🏿‍🚒" + | "🧑‍🚒" + | "👨🏻‍🚒" + | "👨🏼‍🚒" + | "👨🏽‍🚒" + | "👨🏾‍🚒" + | "👨🏿‍🚒" + | "👨‍🚒" + | "👩🏻‍🚒" + | "👩🏼‍🚒" + | "👩🏽‍🚒" + | "👩🏾‍🚒" + | "👩🏿‍🚒" + | "👩‍🚒" + | "👮🏻" + | "👮🏼" + | "👮🏽" + | "👮🏾" + | "👮🏿" + | "👮" + | "👮🏻‍♂️" + | "👮🏼‍♂️" + | "👮🏽‍♂️" + | "👮🏾‍♂️" + | "👮🏿‍♂️" + | "👮‍♂️" + | "👮‍♂" + | "👮🏻‍♀️" + | "👮🏼‍♀️" + | "👮🏽‍♀️" + | "👮🏾‍♀️" + | "👮🏿‍♀️" + | "👮‍♀️" + | "👮‍♀" + | "🕵🏻" + | "🕵🏼" + | "🕵🏽" + | "🕵🏾" + | "🕵🏿" + | "🕵️" + | "🕵" + | "🕵🏻‍♂️" + | "🕵🏼‍♂️" + | "🕵🏽‍♂️" + | "🕵🏾‍♂️" + | "🕵🏿‍♂️" + | "🕵️‍♂️" + | "🕵🏻‍♀️" + | "🕵🏼‍♀️" + | "🕵🏽‍♀️" + | "🕵🏾‍♀️" + | "🕵🏿‍♀️" + | "🕵️‍♀️" + | "💂🏻" + | "💂🏼" + | "💂🏽" + | "💂🏾" + | "💂🏿" + | "💂" + | "💂🏻‍♂️" + | "💂🏼‍♂️" + | "💂🏽‍♂️" + | "💂🏾‍♂️" + | "💂🏿‍♂️" + | "💂‍♂️" + | "💂‍♂" + | "💂🏻‍♀️" + | "💂🏼‍♀️" + | "💂🏽‍♀️" + | "💂🏾‍♀️" + | "💂🏿‍♀️" + | "💂‍♀️" + | "💂‍♀" + | "🥷🏻" + | "🥷🏼" + | "🥷🏽" + | "🥷🏾" + | "🥷🏿" + | "🥷" + | "👷🏻" + | "👷🏼" + | "👷🏽" + | "👷🏾" + | "👷🏿" + | "👷" + | "👷🏻‍♂️" + | "👷🏼‍♂️" + | "👷🏽‍♂️" + | "👷🏾‍♂️" + | "👷🏿‍♂️" + | "👷‍♂️" + | "👷‍♂" + | "👷🏻‍♀️" + | "👷🏼‍♀️" + | "👷🏽‍♀️" + | "👷🏾‍♀️" + | "👷🏿‍♀️" + | "👷‍♀️" + | "👷‍♀" + | "🤴🏻" + | "🤴🏼" + | "🤴🏽" + | "🤴🏾" + | "🤴🏿" + | "🤴" + | "👸🏻" + | "👸🏼" + | "👸🏽" + | "👸🏾" + | "👸🏿" + | "👸" + | "👳🏻" + | "👳🏼" + | "👳🏽" + | "👳🏾" + | "👳🏿" + | "👳" + | "👳🏻‍♂️" + | "👳🏼‍♂️" + | "👳🏽‍♂️" + | "👳🏾‍♂️" + | "👳🏿‍♂️" + | "👳‍♂️" + | "👳‍♂" + | "👳🏻‍♀️" + | "👳🏼‍♀️" + | "👳🏽‍♀️" + | "👳🏾‍♀️" + | "👳🏿‍♀️" + | "👳‍♀️" + | "👳‍♀" + | "👲🏻" + | "👲🏼" + | "👲🏽" + | "👲🏾" + | "👲🏿" + | "👲" + | "🧕🏻" + | "🧕🏼" + | "🧕🏽" + | "🧕🏾" + | "🧕🏿" + | "🧕" + | "🤵🏻" + | "🤵🏼" + | "🤵🏽" + | "🤵🏾" + | "🤵🏿" + | "🤵" + | "🤵🏻‍♂️" + | "🤵🏼‍♂️" + | "🤵🏽‍♂️" + | "🤵🏾‍♂️" + | "🤵🏿‍♂️" + | "🤵‍♂️" + | "🤵‍♂" + | "🤵🏻‍♀️" + | "🤵🏼‍♀️" + | "🤵🏽‍♀️" + | "🤵🏾‍♀️" + | "🤵🏿‍♀️" + | "🤵‍♀️" + | "🤵‍♀" + | "👰🏻" + | "👰🏼" + | "👰🏽" + | "👰🏾" + | "👰🏿" + | "👰" + | "👰🏻‍♂️" + | "👰🏼‍♂️" + | "👰🏽‍♂️" + | "👰🏾‍♂️" + | "👰🏿‍♂️" + | "👰‍♂️" + | "👰‍♂" + | "👰🏻‍♀️" + | "👰🏼‍♀️" + | "👰🏽‍♀️" + | "👰🏾‍♀️" + | "👰🏿‍♀️" + | "👰‍♀️" + | "👰‍♀" + | "🤰🏻" + | "🤰🏼" + | "🤰🏽" + | "🤰🏾" + | "🤰🏿" + | "🤰" + | "🤱🏻" + | "🤱🏼" + | "🤱🏽" + | "🤱🏾" + | "🤱🏿" + | "🤱" + | "👩🏻‍🍼" + | "👩🏼‍🍼" + | "👩🏽‍🍼" + | "👩🏾‍🍼" + | "👩🏿‍🍼" + | "👩‍🍼" + | "👨🏻‍🍼" + | "👨🏼‍🍼" + | "👨🏽‍🍼" + | "👨🏾‍🍼" + | "👨🏿‍🍼" + | "👨‍🍼" + | "🧑🏻‍🍼" + | "🧑🏼‍🍼" + | "🧑🏽‍🍼" + | "🧑🏾‍🍼" + | "🧑🏿‍🍼" + | "🧑‍🍼" + | "👼🏻" + | "👼🏼" + | "👼🏽" + | "👼🏾" + | "👼🏿" + | "👼" + | "🎅🏻" + | "🎅🏼" + | "🎅🏽" + | "🎅🏾" + | "🎅🏿" + | "🎅" + | "🤶🏻" + | "🤶🏼" + | "🤶🏽" + | "🤶🏾" + | "🤶🏿" + | "🤶" + | "🧑🏻‍🎄" + | "🧑🏼‍🎄" + | "🧑🏽‍🎄" + | "🧑🏾‍🎄" + | "🧑🏿‍🎄" + | "🧑‍🎄" + | "🦸🏻" + | "🦸🏼" + | "🦸🏽" + | "🦸🏾" + | "🦸🏿" + | "🦸" + | "🦸🏻‍♂️" + | "🦸🏼‍♂️" + | "🦸🏽‍♂️" + | "🦸🏾‍♂️" + | "🦸🏿‍♂️" + | "🦸‍♂️" + | "🦸‍♂" + | "🦸🏻‍♀️" + | "🦸🏼‍♀️" + | "🦸🏽‍♀️" + | "🦸🏾‍♀️" + | "🦸🏿‍♀️" + | "🦸‍♀️" + | "🦸‍♀" + | "🦹🏻" + | "🦹🏼" + | "🦹🏽" + | "🦹🏾" + | "🦹🏿" + | "🦹" + | "🦹🏻‍♂️" + | "🦹🏼‍♂️" + | "🦹🏽‍♂️" + | "🦹🏾‍♂️" + | "🦹🏿‍♂️" + | "🦹‍♂️" + | "🦹‍♂" + | "🦹🏻‍♀️" + | "🦹🏼‍♀️" + | "🦹🏽‍♀️" + | "🦹🏾‍♀️" + | "🦹🏿‍♀️" + | "🦹‍♀️" + | "🦹‍♀" + | "🧙🏻" + | "🧙🏼" + | "🧙🏽" + | "🧙🏾" + | "🧙🏿" + | "🧙" + | "🧙🏻‍♂️" + | "🧙🏼‍♂️" + | "🧙🏽‍♂️" + | "🧙🏾‍♂️" + | "🧙🏿‍♂️" + | "🧙‍♂️" + | "🧙‍♂" + | "🧙🏻‍♀️" + | "🧙🏼‍♀️" + | "🧙🏽‍♀️" + | "🧙🏾‍♀️" + | "🧙🏿‍♀️" + | "🧙‍♀️" + | "🧙‍♀" + | "🧚🏻" + | "🧚🏼" + | "🧚🏽" + | "🧚🏾" + | "🧚🏿" + | "🧚" + | "🧚🏻‍♂️" + | "🧚🏼‍♂️" + | "🧚🏽‍♂️" + | "🧚🏾‍♂️" + | "🧚🏿‍♂️" + | "🧚‍♂️" + | "🧚‍♂" + | "🧚🏻‍♀️" + | "🧚🏼‍♀️" + | "🧚🏽‍♀️" + | "🧚🏾‍♀️" + | "🧚🏿‍♀️" + | "🧚‍♀️" + | "🧚‍♀" + | "🧛🏻" + | "🧛🏼" + | "🧛🏽" + | "🧛🏾" + | "🧛🏿" + | "🧛" + | "🧛🏻‍♂️" + | "🧛🏼‍♂️" + | "🧛🏽‍♂️" + | "🧛🏾‍♂️" + | "🧛🏿‍♂️" + | "🧛‍♂️" + | "🧛‍♂" + | "🧛🏻‍♀️" + | "🧛🏼‍♀️" + | "🧛🏽‍♀️" + | "🧛🏾‍♀️" + | "🧛🏿‍♀️" + | "🧛‍♀️" + | "🧛‍♀" + | "🧜🏻" + | "🧜🏼" + | "🧜🏽" + | "🧜🏾" + | "🧜🏿" + | "🧜" + | "🧜🏻‍♂️" + | "🧜🏼‍♂️" + | "🧜🏽‍♂️" + | "🧜🏾‍♂️" + | "🧜🏿‍♂️" + | "🧜‍♂️" + | "🧜‍♂" + | "🧜🏻‍♀️" + | "🧜🏼‍♀️" + | "🧜🏽‍♀️" + | "🧜🏾‍♀️" + | "🧜🏿‍♀️" + | "🧜‍♀️" + | "🧜‍♀" + | "🧝🏻" + | "🧝🏼" + | "🧝🏽" + | "🧝🏾" + | "🧝🏿" + | "🧝" + | "🧝🏻‍♂️" + | "🧝🏼‍♂️" + | "🧝🏽‍♂️" + | "🧝🏾‍♂️" + | "🧝🏿‍♂️" + | "🧝‍♂️" + | "🧝‍♂" + | "🧝🏻‍♀️" + | "🧝🏼‍♀️" + | "🧝🏽‍♀️" + | "🧝🏾‍♀️" + | "🧝🏿‍♀️" + | "🧝‍♀️" + | "🧝‍♀" + | "🧞" + | "🧞‍♂️" + | "🧞‍♂" + | "🧞‍♀️" + | "🧞‍♀" + | "🧟" + | "🧟‍♂️" + | "🧟‍♂" + | "🧟‍♀️" + | "🧟‍♀" + | "💆🏻" + | "💆🏼" + | "💆🏽" + | "💆🏾" + | "💆🏿" + | "💆" + | "💆🏻‍♂️" + | "💆🏼‍♂️" + | "💆🏽‍♂️" + | "💆🏾‍♂️" + | "💆🏿‍♂️" + | "💆‍♂️" + | "💆‍♂" + | "💆🏻‍♀️" + | "💆🏼‍♀️" + | "💆🏽‍♀️" + | "💆🏾‍♀️" + | "💆🏿‍♀️" + | "💆‍♀️" + | "💆‍♀" + | "💇🏻" + | "💇🏼" + | "💇🏽" + | "💇🏾" + | "💇🏿" + | "💇" + | "💇🏻‍♂️" + | "💇🏼‍♂️" + | "💇🏽‍♂️" + | "💇🏾‍♂️" + | "💇🏿‍♂️" + | "💇‍♂️" + | "💇‍♂" + | "💇🏻‍♀️" + | "💇🏼‍♀️" + | "💇🏽‍♀️" + | "💇🏾‍♀️" + | "💇🏿‍♀️" + | "💇‍♀️" + | "💇‍♀" + | "🚶🏻" + | "🚶🏼" + | "🚶🏽" + | "🚶🏾" + | "🚶🏿" + | "🚶" + | "🚶🏻‍♂️" + | "🚶🏼‍♂️" + | "🚶🏽‍♂️" + | "🚶🏾‍♂️" + | "🚶🏿‍♂️" + | "🚶‍♂️" + | "🚶‍♂" + | "🚶🏻‍♀️" + | "🚶🏼‍♀️" + | "🚶🏽‍♀️" + | "🚶🏾‍♀️" + | "🚶🏿‍♀️" + | "🚶‍♀️" + | "🚶‍♀" + | "🧍🏻" + | "🧍🏼" + | "🧍🏽" + | "🧍🏾" + | "🧍🏿" + | "🧍" + | "🧍🏻‍♂️" + | "🧍🏼‍♂️" + | "🧍🏽‍♂️" + | "🧍🏾‍♂️" + | "🧍🏿‍♂️" + | "🧍‍♂️" + | "🧍‍♂" + | "🧍🏻‍♀️" + | "🧍🏼‍♀️" + | "🧍🏽‍♀️" + | "🧍🏾‍♀️" + | "🧍🏿‍♀️" + | "🧍‍♀️" + | "🧍‍♀" + | "🧎🏻" + | "🧎🏼" + | "🧎🏽" + | "🧎🏾" + | "🧎🏿" + | "🧎" + | "🧎🏻‍♂️" + | "🧎🏼‍♂️" + | "🧎🏽‍♂️" + | "🧎🏾‍♂️" + | "🧎🏿‍♂️" + | "🧎‍♂️" + | "🧎‍♂" + | "🧎🏻‍♀️" + | "🧎🏼‍♀️" + | "🧎🏽‍♀️" + | "🧎🏾‍♀️" + | "🧎🏿‍♀️" + | "🧎‍♀️" + | "🧎‍♀" + | "🧑🏻‍🦯" + | "🧑🏼‍🦯" + | "🧑🏽‍🦯" + | "🧑🏾‍🦯" + | "🧑🏿‍🦯" + | "🧑‍🦯" + | "👨🏻‍🦯" + | "👨🏼‍🦯" + | "👨🏽‍🦯" + | "👨🏾‍🦯" + | "👨🏿‍🦯" + | "👨‍🦯" + | "👩🏻‍🦯" + | "👩🏼‍🦯" + | "👩🏽‍🦯" + | "👩🏾‍🦯" + | "👩🏿‍🦯" + | "👩‍🦯" + | "🧑🏻‍🦼" + | "🧑🏼‍🦼" + | "🧑🏽‍🦼" + | "🧑🏾‍🦼" + | "🧑🏿‍🦼" + | "🧑‍🦼" + | "👨🏻‍🦼" + | "👨🏼‍🦼" + | "👨🏽‍🦼" + | "👨🏾‍🦼" + | "👨🏿‍🦼" + | "👨‍🦼" + | "👩🏻‍🦼" + | "👩🏼‍🦼" + | "👩🏽‍🦼" + | "👩🏾‍🦼" + | "👩🏿‍🦼" + | "👩‍🦼" + | "🧑🏻‍🦽" + | "🧑🏼‍🦽" + | "🧑🏽‍🦽" + | "🧑🏾‍🦽" + | "🧑🏿‍🦽" + | "🧑‍🦽" + | "👨🏻‍🦽" + | "👨🏼‍🦽" + | "👨🏽‍🦽" + | "👨🏾‍🦽" + | "👨🏿‍🦽" + | "👨‍🦽" + | "👩🏻‍🦽" + | "👩🏼‍🦽" + | "👩🏽‍🦽" + | "👩🏾‍🦽" + | "👩🏿‍🦽" + | "👩‍🦽" + | "🏃🏻" + | "🏃🏼" + | "🏃🏽" + | "🏃🏾" + | "🏃🏿" + | "🏃" + | "🏃🏻‍♂️" + | "🏃🏼‍♂️" + | "🏃🏽‍♂️" + | "🏃🏾‍♂️" + | "🏃🏿‍♂️" + | "🏃‍♂️" + | "🏃‍♂" + | "🏃🏻‍♀️" + | "🏃🏼‍♀️" + | "🏃🏽‍♀️" + | "🏃🏾‍♀️" + | "🏃🏿‍♀️" + | "🏃‍♀️" + | "🏃‍♀" + | "💃🏻" + | "💃🏼" + | "💃🏽" + | "💃🏾" + | "💃🏿" + | "💃" + | "🕺🏻" + | "🕺🏼" + | "🕺🏽" + | "🕺🏾" + | "🕺🏿" + | "🕺" + | "🕴🏻" + | "🕴🏼" + | "🕴🏽" + | "🕴🏾" + | "🕴🏿" + | "🕴️" + | "🕴" + | "👯" + | "👯‍♂️" + | "👯‍♂" + | "👯‍♀️" + | "👯‍♀" + | "🧖🏻" + | "🧖🏼" + | "🧖🏽" + | "🧖🏾" + | "🧖🏿" + | "🧖" + | "🧖🏻‍♂️" + | "🧖🏼‍♂️" + | "🧖🏽‍♂️" + | "🧖🏾‍♂️" + | "🧖🏿‍♂️" + | "🧖‍♂️" + | "🧖‍♂" + | "🧖🏻‍♀️" + | "🧖🏼‍♀️" + | "🧖🏽‍♀️" + | "🧖🏾‍♀️" + | "🧖🏿‍♀️" + | "🧖‍♀️" + | "🧖‍♀" + | "🧗🏻" + | "🧗🏼" + | "🧗🏽" + | "🧗🏾" + | "🧗🏿" + | "🧗" + | "🧗🏻‍♂️" + | "🧗🏼‍♂️" + | "🧗🏽‍♂️" + | "🧗🏾‍♂️" + | "🧗🏿‍♂️" + | "🧗‍♂️" + | "🧗‍♂" + | "🧗🏻‍♀️" + | "🧗🏼‍♀️" + | "🧗🏽‍♀️" + | "🧗🏾‍♀️" + | "🧗🏿‍♀️" + | "🧗‍♀️" + | "🧗‍♀" + | "🤺" + | "🏇🏻" + | "🏇🏼" + | "🏇🏽" + | "🏇🏾" + | "🏇🏿" + | "🏇" + | "⛷️" + | "⛷" + | "🏂🏻" + | "🏂🏼" + | "🏂🏽" + | "🏂🏾" + | "🏂🏿" + | "🏂" + | "🏌🏻" + | "🏌🏼" + | "🏌🏽" + | "🏌🏾" + | "🏌🏿" + | "🏌️" + | "🏌" + | "🏌🏻‍♂️" + | "🏌🏼‍♂️" + | "🏌🏽‍♂️" + | "🏌🏾‍♂️" + | "🏌🏿‍♂️" + | "🏌️‍♂️" + | "🏌🏻‍♀️" + | "🏌🏼‍♀️" + | "🏌🏽‍♀️" + | "🏌🏾‍♀️" + | "🏌🏿‍♀️" + | "🏌️‍♀️" + | "🏄🏻" + | "🏄🏼" + | "🏄🏽" + | "🏄🏾" + | "🏄🏿" + | "🏄" + | "🏄🏻‍♂️" + | "🏄🏼‍♂️" + | "🏄🏽‍♂️" + | "🏄🏾‍♂️" + | "🏄🏿‍♂️" + | "🏄‍♂️" + | "🏄‍♂" + | "🏄🏻‍♀️" + | "🏄🏼‍♀️" + | "🏄🏽‍♀️" + | "🏄🏾‍♀️" + | "🏄🏿‍♀️" + | "🏄‍♀️" + | "🏄‍♀" + | "🚣🏻" + | "🚣🏼" + | "🚣🏽" + | "🚣🏾" + | "🚣🏿" + | "🚣" + | "🚣🏻‍♂️" + | "🚣🏼‍♂️" + | "🚣🏽‍♂️" + | "🚣🏾‍♂️" + | "🚣🏿‍♂️" + | "🚣‍♂️" + | "🚣‍♂" + | "🚣🏻‍♀️" + | "🚣🏼‍♀️" + | "🚣🏽‍♀️" + | "🚣🏾‍♀️" + | "🚣🏿‍♀️" + | "🚣‍♀️" + | "🚣‍♀" + | "🏊🏻" + | "🏊🏼" + | "🏊🏽" + | "🏊🏾" + | "🏊🏿" + | "🏊" + | "🏊🏻‍♂️" + | "🏊🏼‍♂️" + | "🏊🏽‍♂️" + | "🏊🏾‍♂️" + | "🏊🏿‍♂️" + | "🏊‍♂️" + | "🏊‍♂" + | "🏊🏻‍♀️" + | "🏊🏼‍♀️" + | "🏊🏽‍♀️" + | "🏊🏾‍♀️" + | "🏊🏿‍♀️" + | "🏊‍♀️" + | "🏊‍♀" + | "⛹🏻" + | "⛹🏼" + | "⛹🏽" + | "⛹🏾" + | "⛹🏿" + | "⛹️" + | "⛹" + | "⛹🏻‍♂️" + | "⛹🏼‍♂️" + | "⛹🏽‍♂️" + | "⛹🏾‍♂️" + | "⛹🏿‍♂️" + | "⛹️‍♂️" + | "⛹🏻‍♀️" + | "⛹🏼‍♀️" + | "⛹🏽‍♀️" + | "⛹🏾‍♀️" + | "⛹🏿‍♀️" + | "⛹️‍♀️" + | "🏋🏻" + | "🏋🏼" + | "🏋🏽" + | "🏋🏾" + | "🏋🏿" + | "🏋️" + | "🏋" + | "🏋🏻‍♂️" + | "🏋🏼‍♂️" + | "🏋🏽‍♂️" + | "🏋🏾‍♂️" + | "🏋🏿‍♂️" + | "🏋️‍♂️" + | "🏋🏻‍♀️" + | "🏋🏼‍♀️" + | "🏋🏽‍♀️" + | "🏋🏾‍♀️" + | "🏋🏿‍♀️" + | "🏋️‍♀️" + | "🚴🏻" + | "🚴🏼" + | "🚴🏽" + | "🚴🏾" + | "🚴🏿" + | "🚴" + | "🚴🏻‍♂️" + | "🚴🏼‍♂️" + | "🚴🏽‍♂️" + | "🚴🏾‍♂️" + | "🚴🏿‍♂️" + | "🚴‍♂️" + | "🚴‍♂" + | "🚴🏻‍♀️" + | "🚴🏼‍♀️" + | "🚴🏽‍♀️" + | "🚴🏾‍♀️" + | "🚴🏿‍♀️" + | "🚴‍♀️" + | "🚴‍♀" + | "🚵🏻" + | "🚵🏼" + | "🚵🏽" + | "🚵🏾" + | "🚵🏿" + | "🚵" + | "🚵🏻‍♂️" + | "🚵🏼‍♂️" + | "🚵🏽‍♂️" + | "🚵🏾‍♂️" + | "🚵🏿‍♂️" + | "🚵‍♂️" + | "🚵‍♂" + | "🚵🏻‍♀️" + | "🚵🏼‍♀️" + | "🚵🏽‍♀️" + | "🚵🏾‍♀️" + | "🚵🏿‍♀️" + | "🚵‍♀️" + | "🚵‍♀" + | "🤸🏻" + | "🤸🏼" + | "🤸🏽" + | "🤸🏾" + | "🤸🏿" + | "🤸" + | "🤸🏻‍♂️" + | "🤸🏼‍♂️" + | "🤸🏽‍♂️" + | "🤸🏾‍♂️" + | "🤸🏿‍♂️" + | "🤸‍♂️" + | "🤸‍♂" + | "🤸🏻‍♀️" + | "🤸🏼‍♀️" + | "🤸🏽‍♀️" + | "🤸🏾‍♀️" + | "🤸🏿‍♀️" + | "🤸‍♀️" + | "🤸‍♀" + | "🤼" + | "🤼‍♂️" + | "🤼‍♂" + | "🤼‍♀️" + | "🤼‍♀" + | "🤽🏻" + | "🤽🏼" + | "🤽🏽" + | "🤽🏾" + | "🤽🏿" + | "🤽" + | "🤽🏻‍♂️" + | "🤽🏼‍♂️" + | "🤽🏽‍♂️" + | "🤽🏾‍♂️" + | "🤽🏿‍♂️" + | "🤽‍♂️" + | "🤽‍♂" + | "🤽🏻‍♀️" + | "🤽🏼‍♀️" + | "🤽🏽‍♀️" + | "🤽🏾‍♀️" + | "🤽🏿‍♀️" + | "🤽‍♀️" + | "🤽‍♀" + | "🤾🏻" + | "🤾🏼" + | "🤾🏽" + | "🤾🏾" + | "🤾🏿" + | "🤾" + | "🤾🏻‍♂️" + | "🤾🏼‍♂️" + | "🤾🏽‍♂️" + | "🤾🏾‍♂️" + | "🤾🏿‍♂️" + | "🤾‍♂️" + | "🤾‍♂" + | "🤾🏻‍♀️" + | "🤾🏼‍♀️" + | "🤾🏽‍♀️" + | "🤾🏾‍♀️" + | "🤾🏿‍♀️" + | "🤾‍♀️" + | "🤾‍♀" + | "🤹🏻" + | "🤹🏼" + | "🤹🏽" + | "🤹🏾" + | "🤹🏿" + | "🤹" + | "🤹🏻‍♂️" + | "🤹🏼‍♂️" + | "🤹🏽‍♂️" + | "🤹🏾‍♂️" + | "🤹🏿‍♂️" + | "🤹‍♂️" + | "🤹‍♂" + | "🤹🏻‍♀️" + | "🤹🏼‍♀️" + | "🤹🏽‍♀️" + | "🤹🏾‍♀️" + | "🤹🏿‍♀️" + | "🤹‍♀️" + | "🤹‍♀" + | "🧘🏻" + | "🧘🏼" + | "🧘🏽" + | "🧘🏾" + | "🧘🏿" + | "🧘" + | "🧘🏻‍♂️" + | "🧘🏼‍♂️" + | "🧘🏽‍♂️" + | "🧘🏾‍♂️" + | "🧘🏿‍♂️" + | "🧘‍♂️" + | "🧘‍♂" + | "🧘🏻‍♀️" + | "🧘🏼‍♀️" + | "🧘🏽‍♀️" + | "🧘🏾‍♀️" + | "🧘🏿‍♀️" + | "🧘‍♀️" + | "🧘‍♀" + | "🛀🏻" + | "🛀🏼" + | "🛀🏽" + | "🛀🏾" + | "🛀🏿" + | "🛀" + | "🛌🏻" + | "🛌🏼" + | "🛌🏽" + | "🛌🏾" + | "🛌🏿" + | "🛌" + | "🧑🏻‍🤝‍🧑🏻" + | "🧑🏻‍🤝‍🧑🏼" + | "🧑🏻‍🤝‍🧑🏽" + | "🧑🏻‍🤝‍🧑🏾" + | "🧑🏻‍🤝‍🧑🏿" + | "🧑🏼‍🤝‍🧑🏻" + | "🧑🏼‍🤝‍🧑🏼" + | "🧑🏼‍🤝‍🧑🏽" + | "🧑🏼‍🤝‍🧑🏾" + | "🧑🏼‍🤝‍🧑🏿" + | "🧑🏽‍🤝‍🧑🏻" + | "🧑🏽‍🤝‍🧑🏼" + | "🧑🏽‍🤝‍🧑🏽" + | "🧑🏽‍🤝‍🧑🏾" + | "🧑🏽‍🤝‍🧑🏿" + | "🧑🏾‍🤝‍🧑🏻" + | "🧑🏾‍🤝‍🧑🏼" + | "🧑🏾‍🤝‍🧑🏽" + | "🧑🏾‍🤝‍🧑🏾" + | "🧑🏾‍🤝‍🧑🏿" + | "🧑🏿‍🤝‍🧑🏻" + | "🧑🏿‍🤝‍🧑🏼" + | "🧑🏿‍🤝‍🧑🏽" + | "🧑🏿‍🤝‍🧑🏾" + | "🧑🏿‍🤝‍🧑🏿" + | "🧑‍🤝‍🧑" + | "👭" + | "👫" + | "👬" + | "💏" + | "💑" + | "👪" + | "👨‍👩‍👦" + | "👨‍👩‍👧" + | "👨‍👩‍👧‍👦" + | "👨‍👩‍👦‍👦" + | "👨‍👩‍👧‍👧" + | "👨‍👨‍👦" + | "👨‍👨‍👧" + | "👨‍👨‍👧‍👦" + | "👨‍👨‍👦‍👦" + | "👨‍👨‍👧‍👧" + | "👩‍👩‍👦" + | "👩‍👩‍👧" + | "👩‍👩‍👧‍👦" + | "👩‍👩‍👦‍👦" + | "👩‍👩‍👧‍👧" + | "👨‍👦" + | "👨‍👦‍👦" + | "👨‍👧" + | "👨‍👧‍👦" + | "👨‍👧‍👧" + | "👩‍👦" + | "👩‍👦‍👦" + | "👩‍👧" + | "👩‍👧‍👦" + | "👩‍👧‍👧" + | "🗣️" + | "🗣" + | "👤" + | "👥" + | "🫂" + | "👣" + | "🐵" + | "🐒" + | "🦍" + | "🦧" + | "🐶" + | "🐕" + | "🦮" + | "🐕‍🦺" + | "🐩" + | "🐺" + | "🦊" + | "🦝" + | "🐱" + | "🐈" + | "🐈‍⬛" + | "🦁" + | "🐯" + | "🐅" + | "🐆" + | "🐴" + | "🐎" + | "🦄" + | "🦓" + | "🦌" + | "🦬" + | "🐮" + | "🐂" + | "🐃" + | "🐄" + | "🐷" + | "🐖" + | "🐗" + | "🐽" + | "🐏" + | "🐑" + | "🐐" + | "🐪" + | "🐫" + | "🦙" + | "🦒" + | "🐘" + | "🦣" + | "🦏" + | "🦛" + | "🐭" + | "🐁" + | "🐀" + | "🐹" + | "🐰" + | "🐇" + | "🐿️" + | "🐿" + | "🦫" + | "🦔" + | "🦇" + | "🐻" + | "🐻‍❄️" + | "🐻‍❄" + | "🐨" + | "🐼" + | "🦥" + | "🦦" + | "🦨" + | "🦘" + | "🦡" + | "🐾" + | "🦃" + | "🐔" + | "🐓" + | "🐣" + | "🐤" + | "🐥" + | "🐦" + | "🐧" + | "🕊️" + | "🕊" + | "🦅" + | "🦆" + | "🦢" + | "🦉" + | "🦤" + | "🪶" + | "🦩" + | "🦚" + | "🦜" + | "🐸" + | "🐊" + | "🐢" + | "🦎" + | "🐍" + | "🐲" + | "🐉" + | "🦕" + | "🦖" + | "🐳" + | "🐋" + | "🐬" + | "🦭" + | "🐟" + | "🐠" + | "🐡" + | "🦈" + | "🐙" + | "🐚" + | "🐌" + | "🦋" + | "🐛" + | "🐜" + | "🐝" + | "🪲" + | "🐞" + | "🦗" + | "🪳" + | "🕷️" + | "🕷" + | "🕸️" + | "🕸" + | "🦂" + | "🦟" + | "🪰" + | "🪱" + | "🦠" + | "💐" + | "🌸" + | "💮" + | "🏵️" + | "🏵" + | "🌹" + | "🥀" + | "🌺" + | "🌻" + | "🌼" + | "🌷" + | "🌱" + | "🪴" + | "🌲" + | "🌳" + | "🌴" + | "🌵" + | "🌾" + | "🌿" + | "☘️" + | "☘" + | "🍀" + | "🍁" + | "🍂" + | "🍃" + | "🍇" + | "🍈" + | "🍉" + | "🍊" + | "🍋" + | "🍌" + | "🍍" + | "🥭" + | "🍎" + | "🍏" + | "🍐" + | "🍑" + | "🍒" + | "🍓" + | "🫐" + | "🥝" + | "🍅" + | "🫒" + | "🥥" + | "🥑" + | "🍆" + | "🥔" + | "🥕" + | "🌽" + | "🌶️" + | "🌶" + | "🫑" + | "🥒" + | "🥬" + | "🥦" + | "🧄" + | "🧅" + | "🍄" + | "🥜" + | "🌰" + | "🍞" + | "🥐" + | "🥖" + | "🫓" + | "🥨" + | "🥯" + | "🥞" + | "🧇" + | "🧀" + | "🍖" + | "🍗" + | "🥩" + | "🥓" + | "🍔" + | "🍟" + | "🍕" + | "🌭" + | "🥪" + | "🌮" + | "🌯" + | "🫔" + | "🥙" + | "🧆" + | "🥚" + | "🍳" + | "🥘" + | "🍲" + | "🫕" + | "🥣" + | "🥗" + | "🍿" + | "🧈" + | "🧂" + | "🥫" + | "🍱" + | "🍘" + | "🍙" + | "🍚" + | "🍛" + | "🍜" + | "🍝" + | "🍠" + | "🍢" + | "🍣" + | "🍤" + | "🍥" + | "🥮" + | "🍡" + | "🥟" + | "🥠" + | "🥡" + | "🦀" + | "🦞" + | "🦐" + | "🦑" + | "🦪" + | "🍦" + | "🍧" + | "🍨" + | "🍩" + | "🍪" + | "🎂" + | "🍰" + | "🧁" + | "🥧" + | "🍫" + | "🍬" + | "🍭" + | "🍮" + | "🍯" + | "🍼" + | "🥛" + | "☕" + | "🫖" + | "🍵" + | "🍶" + | "🍾" + | "🍷" + | "🍸" + | "🍹" + | "🍺" + | "🍻" + | "🥂" + | "🥃" + | "🥤" + | "🧋" + | "🧃" + | "🧉" + | "🧊" + | "🥢" + | "🍽️" + | "🍽" + | "🍴" + | "🥄" + | "🔪" + | "🏺" + | "🌍" + | "🌎" + | "🌏" + | "🌐" + | "🗺️" + | "🗺" + | "🗾" + | "🧭" + | "🏔️" + | "🏔" + | "⛰️" + | "⛰" + | "🌋" + | "🗻" + | "🏕️" + | "🏕" + | "🏖️" + | "🏖" + | "🏜️" + | "🏜" + | "🏝️" + | "🏝" + | "🏞️" + | "🏞" + | "🏟️" + | "🏟" + | "🏛️" + | "🏛" + | "🏗️" + | "🏗" + | "🧱" + | "🪨" + | "🪵" + | "🛖" + | "🏘️" + | "🏘" + | "🏚️" + | "🏚" + | "🏠" + | "🏡" + | "🏢" + | "🏣" + | "🏤" + | "🏥" + | "🏦" + | "🏨" + | "🏩" + | "🏪" + | "🏫" + | "🏬" + | "🏭" + | "🏯" + | "🏰" + | "💒" + | "🗼" + | "🗽" + | "⛪" + | "🕌" + | "🛕" + | "🕍" + | "⛩️" + | "⛩" + | "🕋" + | "⛲" + | "⛺" + | "🌁" + | "🌃" + | "🏙️" + | "🏙" + | "🌄" + | "🌅" + | "🌆" + | "🌇" + | "🌉" + | "♨️" + | "♨" + | "🎠" + | "🎡" + | "🎢" + | "💈" + | "🎪" + | "🚂" + | "🚃" + | "🚄" + | "🚅" + | "🚆" + | "🚇" + | "🚈" + | "🚉" + | "🚊" + | "🚝" + | "🚞" + | "🚋" + | "🚌" + | "🚍" + | "🚎" + | "🚐" + | "🚑" + | "🚒" + | "🚓" + | "🚔" + | "🚕" + | "🚖" + | "🚗" + | "🚘" + | "🚙" + | "🛻" + | "🚚" + | "🚛" + | "🚜" + | "🏎️" + | "🏎" + | "🏍️" + | "🏍" + | "🛵" + | "🦽" + | "🦼" + | "🛺" + | "🚲" + | "🛴" + | "🛹" + | "🛼" + | "🚏" + | "🛣️" + | "🛣" + | "🛤️" + | "🛤" + | "🛢️" + | "🛢" + | "⛽" + | "🚨" + | "🚥" + | "🚦" + | "🛑" + | "🚧" + | "⚓" + | "⛵" + | "🛶" + | "🚤" + | "🛳️" + | "🛳" + | "⛴️" + | "⛴" + | "🛥️" + | "🛥" + | "🚢" + | "✈️" + | "✈" + | "🛩️" + | "🛩" + | "🛫" + | "🛬" + | "🪂" + | "💺" + | "🚁" + | "🚟" + | "🚠" + | "🚡" + | "🛰️" + | "🛰" + | "🚀" + | "🛸" + | "🛎️" + | "🛎" + | "🧳" + | "⌛" + | "⏳" + | "⌚" + | "⏰" + | "⏱️" + | "⏱" + | "⏲️" + | "⏲" + | "🕰️" + | "🕰" + | "🕛" + | "🕧" + | "🕐" + | "🕜" + | "🕑" + | "🕝" + | "🕒" + | "🕞" + | "🕓" + | "🕟" + | "🕔" + | "🕠" + | "🕕" + | "🕡" + | "🕖" + | "🕢" + | "🕗" + | "🕣" + | "🕘" + | "🕤" + | "🕙" + | "🕥" + | "🕚" + | "🕦" + | "🌑" + | "🌒" + | "🌓" + | "🌔" + | "🌕" + | "🌖" + | "🌗" + | "🌘" + | "🌙" + | "🌚" + | "🌛" + | "🌜" + | "🌡️" + | "🌡" + | "☀️" + | "☀" + | "🌝" + | "🌞" + | "🪐" + | "⭐" + | "🌟" + | "🌠" + | "🌌" + | "☁️" + | "☁" + | "⛅" + | "⛈️" + | "⛈" + | "🌤️" + | "🌤" + | "🌥️" + | "🌥" + | "🌦️" + | "🌦" + | "🌧️" + | "🌧" + | "🌨️" + | "🌨" + | "🌩️" + | "🌩" + | "🌪️" + | "🌪" + | "🌫️" + | "🌫" + | "🌬️" + | "🌬" + | "🌀" + | "🌈" + | "🌂" + | "☂️" + | "☂" + | "☔" + | "⛱️" + | "⛱" + | "⚡" + | "❄️" + | "❄" + | "☃️" + | "☃" + | "⛄" + | "☄️" + | "☄" + | "🔥" + | "💧" + | "🌊" + | "🎃" + | "🎄" + | "🎆" + | "🎇" + | "🧨" + | "✨" + | "🎈" + | "🎉" + | "🎊" + | "🎋" + | "🎍" + | "🎎" + | "🎏" + | "🎐" + | "🎑" + | "🧧" + | "🎀" + | "🎁" + | "🎗️" + | "🎗" + | "🎟️" + | "🎟" + | "🎫" + | "🎖️" + | "🎖" + | "🏆" + | "🏅" + | "🥇" + | "🥈" + | "🥉" + | "⚽" + | "⚾" + | "🥎" + | "🏀" + | "🏐" + | "🏈" + | "🏉" + | "🎾" + | "🥏" + | "🎳" + | "🏏" + | "🏑" + | "🏒" + | "🥍" + | "🏓" + | "🏸" + | "🥊" + | "🥋" + | "🥅" + | "⛳" + | "⛸️" + | "⛸" + | "🎣" + | "🤿" + | "🎽" + | "🎿" + | "🛷" + | "🥌" + | "🎯" + | "🪀" + | "🪁" + | "🎱" + | "🔮" + | "🪄" + | "🧿" + | "🎮" + | "🕹️" + | "🕹" + | "🎰" + | "🎲" + | "🧩" + | "🧸" + | "🪅" + | "🪆" + | "♠️" + | "♠" + | "♥️" + | "♥" + | "♦️" + | "♦" + | "♣️" + | "♣" + | "♟️" + | "♟" + | "🃏" + | "🀄" + | "🎴" + | "🎭" + | "🖼️" + | "🖼" + | "🎨" + | "🧵" + | "🪡" + | "🧶" + | "🪢" + | "👓" + | "🕶️" + | "🕶" + | "🥽" + | "🥼" + | "🦺" + | "👔" + | "👕" + | "👖" + | "🧣" + | "🧤" + | "🧥" + | "🧦" + | "👗" + | "👘" + | "🥻" + | "🩱" + | "🩲" + | "🩳" + | "👙" + | "👚" + | "👛" + | "👜" + | "👝" + | "🛍️" + | "🛍" + | "🎒" + | "🩴" + | "👞" + | "👟" + | "🥾" + | "🥿" + | "👠" + | "👡" + | "🩰" + | "👢" + | "👑" + | "👒" + | "🎩" + | "🎓" + | "🧢" + | "🪖" + | "⛑️" + | "⛑" + | "📿" + | "💄" + | "💍" + | "💎" + | "🔇" + | "🔈" + | "🔉" + | "🔊" + | "📢" + | "📣" + | "📯" + | "🔔" + | "🔕" + | "🎼" + | "🎵" + | "🎶" + | "🎙️" + | "🎙" + | "🎚️" + | "🎚" + | "🎛️" + | "🎛" + | "🎤" + | "🎧" + | "📻" + | "🎷" + | "🪗" + | "🎸" + | "🎹" + | "🎺" + | "🎻" + | "🪕" + | "🥁" + | "🪘" + | "📱" + | "📲" + | "☎️" + | "☎" + | "📞" + | "📟" + | "📠" + | "🔋" + | "🔌" + | "💻" + | "🖥️" + | "🖥" + | "🖨️" + | "🖨" + | "⌨️" + | "⌨" + | "🖱️" + | "🖱" + | "🖲️" + | "🖲" + | "💽" + | "💾" + | "💿" + | "📀" + | "🧮" + | "🎥" + | "🎞️" + | "🎞" + | "📽️" + | "📽" + | "🎬" + | "📺" + | "📷" + | "📸" + | "📹" + | "📼" + | "🔍" + | "🔎" + | "🕯️" + | "🕯" + | "💡" + | "🔦" + | "🏮" + | "🪔" + | "📔" + | "📕" + | "📖" + | "📗" + | "📘" + | "📙" + | "📚" + | "📓" + | "📒" + | "📃" + | "📜" + | "📄" + | "📰" + | "🗞️" + | "🗞" + | "📑" + | "🔖" + | "🏷️" + | "🏷" + | "💰" + | "🪙" + | "💴" + | "💵" + | "💶" + | "💷" + | "💸" + | "💳" + | "🧾" + | "💹" + | "✉️" + | "✉" + | "📧" + | "📨" + | "📩" + | "📤" + | "📥" + | "📦" + | "📫" + | "📪" + | "📬" + | "📭" + | "📮" + | "🗳️" + | "🗳" + | "✏️" + | "✏" + | "✒️" + | "✒" + | "🖋️" + | "🖋" + | "🖊️" + | "🖊" + | "🖌️" + | "🖌" + | "🖍️" + | "🖍" + | "📝" + | "💼" + | "📁" + | "📂" + | "🗂️" + | "🗂" + | "📅" + | "📆" + | "🗒️" + | "🗒" + | "🗓️" + | "🗓" + | "📇" + | "📈" + | "📉" + | "📊" + | "📋" + | "📌" + | "📍" + | "📎" + | "🖇️" + | "🖇" + | "📏" + | "📐" + | "✂️" + | "✂" + | "🗃️" + | "🗃" + | "🗄️" + | "🗄" + | "🗑️" + | "🗑" + | "🔒" + | "🔓" + | "🔏" + | "🔐" + | "🔑" + | "🗝️" + | "🗝" + | "🔨" + | "🪓" + | "⛏️" + | "⛏" + | "⚒️" + | "⚒" + | "🛠️" + | "🛠" + | "🗡️" + | "🗡" + | "⚔️" + | "⚔" + | "🔫" + | "🪃" + | "🏹" + | "🛡️" + | "🛡" + | "🪚" + | "🔧" + | "🪛" + | "🔩" + | "⚙️" + | "⚙" + | "🗜️" + | "🗜" + | "⚖️" + | "⚖" + | "🦯" + | "🔗" + | "⛓️" + | "⛓" + | "🪝" + | "🧰" + | "🧲" + | "🪜" + | "⚗️" + | "⚗" + | "🧪" + | "🧫" + | "🧬" + | "🔬" + | "🔭" + | "📡" + | "💉" + | "🩸" + | "💊" + | "🩹" + | "🩺" + | "🚪" + | "🛗" + | "🪞" + | "🪟" + | "🛏️" + | "🛏" + | "🛋️" + | "🛋" + | "🪑" + | "🚽" + | "🪠" + | "🚿" + | "🛁" + | "🪤" + | "🪒" + | "🧴" + | "🧷" + | "🧹" + | "🧺" + | "🧻" + | "🪣" + | "🧼" + | "🪥" + | "🧽" + | "🧯" + | "🛒" + | "🚬" + | "⚰️" + | "⚰" + | "🪦" + | "⚱️" + | "⚱" + | "🗿" + | "🪧" + | "🏧" + | "🚮" + | "🚰" + | "♿" + | "🚹" + | "🚺" + | "🚻" + | "🚼" + | "🚾" + | "🛂" + | "🛃" + | "🛄" + | "🛅" + | "⚠️" + | "⚠" + | "🚸" + | "⛔" + | "🚫" + | "🚳" + | "🚭" + | "🚯" + | "🚱" + | "🚷" + | "📵" + | "🔞" + | "☢️" + | "☢" + | "☣️" + | "☣" + | "⬆️" + | "⬆" + | "↗️" + | "↗" + | "➡️" + | "➡" + | "↘️" + | "↘" + | "⬇️" + | "⬇" + | "↙️" + | "↙" + | "⬅️" + | "⬅" + | "↖️" + | "↖" + | "↕️" + | "↕" + | "↔️" + | "↩️" + | "↩" + | "↪️" + | "↪" + | "⤴️" + | "⤴" + | "⤵️" + | "⤵" + | "🔃" + | "🔄" + | "🔙" + | "🔚" + | "🔛" + | "🔜" + | "🔝" + | "🛐" + | "⚛️" + | "⚛" + | "🕉️" + | "🕉" + | "✡️" + | "✡" + | "☸️" + | "☸" + | "☯️" + | "☯" + | "✝️" + | "✝" + | "☦️" + | "☦" + | "☪️" + | "☪" + | "☮️" + | "☮" + | "🕎" + | "🔯" + | "♈" + | "♉" + | "♊" + | "♋" + | "♌" + | "♍" + | "♎" + | "♏" + | "♐" + | "♑" + | "♒" + | "♓" + | "⛎" + | "🔀" + | "🔁" + | "🔂" + | "▶️" + | "⏩" + | "⏭️" + | "⏭" + | "⏯️" + | "⏯" + | "◀️" + | "⏪" + | "⏮️" + | "⏮" + | "🔼" + | "⏫" + | "🔽" + | "⏬" + | "⏸️" + | "⏸" + | "⏹️" + | "⏹" + | "⏺️" + | "⏺" + | "⏏️" + | "⏏" + | "🎦" + | "🔅" + | "🔆" + | "📶" + | "📳" + | "📴" + | "♀️" + | "♀" + | "♂️" + | "♂" + | "⚧️" + | "⚧" + | "✖️" + | "✖" + | "➕" + | "➖" + | "➗" + | "♾️" + | "♾" + | "‼️" + | "‼" + | "⁉️" + | "⁉" + | "❓" + | "❔" + | "❕" + | "❗" + | "〰️" + | "〰" + | "💱" + | "💲" + | "⚕️" + | "⚕" + | "♻️" + | "♻" + | "⚜️" + | "⚜" + | "🔱" + | "📛" + | "🔰" + | "⭕" + | "✅" + | "☑️" + | "☑" + | "✔️" + | "✔" + | "❌" + | "❎" + | "➰" + | "➿" + | "〽️" + | "〽" + | "✳️" + | "✳" + | "✴️" + | "✴" + | "❇️" + | "❇" + | "©️" + | "©" + | "®️" + | "®" + | "™️" + | "#️⃣" + | "#⃣" + | "*️⃣" + | "*⃣" + | "0️⃣" + | "0⃣" + | "1️⃣" + | "1⃣" + | "2️⃣" + | "2⃣" + | "3️⃣" + | "3⃣" + | "4️⃣" + | "4⃣" + | "5️⃣" + | "5⃣" + | "6️⃣" + | "6⃣" + | "7️⃣" + | "7⃣" + | "8️⃣" + | "8⃣" + | "9️⃣" + | "9⃣" + | "🔟" + | "🔠" + | "🔡" + | "🔢" + | "🔣" + | "🔤" + | "🅰️" + | "🅰" + | "🆎" + | "🅱️" + | "🅱" + | "🆑" + | "🆒" + | "🆓" + | "ℹ️" + | "ℹ" + | "🆔" + | "Ⓜ️" + | "Ⓜ" + | "🆕" + | "🆖" + | "🅾️" + | "🅾" + | "🆗" + | "🅿️" + | "🅿" + | "🆘" + | "🆙" + | "🆚" + | "🈁" + | "🈂️" + | "🈂" + | "🈷️" + | "🈷" + | "🈶" + | "🈯" + | "🉐" + | "🈹" + | "🈚" + | "🈲" + | "🉑" + | "🈸" + | "🈴" + | "🈳" + | "㊗️" + | "㊗" + | "㊙️" + | "㊙" + | "🈺" + | "🈵" + | "🔴" + | "🟠" + | "🟡" + | "🟢" + | "🔵" + | "🟣" + | "🟤" + | "⚫" + | "⚪" + | "🟥" + | "🟧" + | "🟨" + | "🟩" + | "🟦" + | "🟪" + | "🟫" + | "⬛" + | "⬜" + | "◼️" + | "◼" + | "◻️" + | "◻" + | "◾" + | "◽" + | "▪️" + | "▪" + | "▫️" + | "▫" + | "🔶" + | "🔷" + | "🔸" + | "🔹" + | "🔺" + | "🔻" + | "💠" + | "🔘" + | "🔳" + | "🔲" + | "🏁" + | "🚩" + | "🎌" + | "🏴" + | "🏳️" + | "🏳" + | "🏳️‍🌈" + | "🏳‍🌈" + | "🏳️‍⚧️" + | "🏴‍☠️" + | "🏴‍☠" + | "🇦🇨" + | "🇦🇩" + | "🇦🇪" + | "🇦🇫" + | "🇦🇬" + | "🇦🇮" + | "🇦🇱" + | "🇦🇲" + | "🇦🇴" + | "🇦🇶" + | "🇦🇷" + | "🇦🇸" + | "🇦🇹" + | "🇦🇺" + | "🇦🇼" + | "🇦🇽" + | "🇦🇿" + | "🇧🇦" + | "🇧🇧" + | "🇧🇩" + | "🇧🇪" + | "🇧🇫" + | "🇧🇬" + | "🇧🇭" + | "🇧🇮" + | "🇧🇯" + | "🇧🇱" + | "🇧🇲" + | "🇧🇳" + | "🇧🇴" + | "🇧🇶" + | "🇧🇷" + | "🇧🇸" + | "🇧🇹" + | "🇧🇻" + | "🇧🇼" + | "🇧🇾" + | "🇧🇿" + | "🇨🇦" + | "🇨🇨" + | "🇨🇩" + | "🇨🇫" + | "🇨🇬" + | "🇨🇭" + | "🇨🇮" + | "🇨🇰" + | "🇨🇱" + | "🇨🇲" + | "🇨🇳" + | "🇨🇴" + | "🇨🇵" + | "🇨🇷" + | "🇨🇺" + | "🇨🇻" + | "🇨🇼" + | "🇨🇽" + | "🇨🇾" + | "🇨🇿" + | "🇩🇪" + | "🇩🇬" + | "🇩🇯" + | "🇩🇰" + | "🇩🇲" + | "🇩🇴" + | "🇩🇿" + | "🇪🇦" + | "🇪🇨" + | "🇪🇪" + | "🇪🇬" + | "🇪🇭" + | "🇪🇷" + | "🇪🇸" + | "🇪🇹" + | "🇪🇺" + | "🇫🇮" + | "🇫🇯" + | "🇫🇰" + | "🇫🇲" + | "🇫🇴" + | "🇫🇷" + | "🇬🇦" + | "🇬🇧" + | "🇬🇩" + | "🇬🇪" + | "🇬🇫" + | "🇬🇬" + | "🇬🇭" + | "🇬🇮" + | "🇬🇱" + | "🇬🇲" + | "🇬🇳" + | "🇬🇵" + | "🇬🇶" + | "🇬🇷" + | "🇬🇸" + | "🇬🇹" + | "🇬🇺" + | "🇬🇼" + | "🇬🇾" + | "🇭🇰" + | "🇭🇲" + | "🇭🇳" + | "🇭🇷" + | "🇭🇹" + | "🇭🇺" + | "🇮🇨" + | "🇮🇩" + | "🇮🇪" + | "🇮🇱" + | "🇮🇲" + | "🇮🇳" + | "🇮🇴" + | "🇮🇶" + | "🇮🇷" + | "🇮🇸" + | "🇮🇹" + | "🇯🇪" + | "🇯🇲" + | "🇯🇴" + | "🇯🇵" + | "🇰🇪" + | "🇰🇬" + | "🇰🇭" + | "🇰🇮" + | "🇰🇲" + | "🇰🇳" + | "🇰🇵" + | "🇰🇷" + | "🇰🇼" + | "🇰🇾" + | "🇰🇿" + | "🇱🇦" + | "🇱🇧" + | "🇱🇨" + | "🇱🇮" + | "🇱🇰" + | "🇱🇷" + | "🇱🇸" + | "🇱🇹" + | "🇱🇺" + | "🇱🇻" + | "🇱🇾" + | "🇲🇦" + | "🇲🇨" + | "🇲🇩" + | "🇲🇪" + | "🇲🇫" + | "🇲🇬" + | "🇲🇭" + | "🇲🇰" + | "🇲🇱" + | "🇲🇲" + | "🇲🇳" + | "🇲🇴" + | "🇲🇵" + | "🇲🇶" + | "🇲🇷" + | "🇲🇸" + | "🇲🇹" + | "🇲🇺" + | "🇲🇻" + | "🇲🇼" + | "🇲🇽" + | "🇲🇾" + | "🇲🇿" + | "🇳🇦" + | "🇳🇨" + | "🇳🇪" + | "🇳🇫" + | "🇳🇬" + | "🇳🇮" + | "🇳🇱" + | "🇳🇴" + | "🇳🇵" + | "🇳🇷" + | "🇳🇺" + | "🇳🇿" + | "🇴🇲" + | "🇵🇦" + | "🇵🇪" + | "🇵🇫" + | "🇵🇬" + | "🇵🇭" + | "🇵🇰" + | "🇵🇱" + | "🇵🇲" + | "🇵🇳" + | "🇵🇷" + | "🇵🇸" + | "🇵🇹" + | "🇵🇼" + | "🇵🇾" + | "🇶🇦" + | "🇷🇪" + | "🇷🇴" + | "🇷🇸" + | "🇷🇺" + | "🇷🇼" + | "🇸🇦" + | "🇸🇧" + | "🇸🇨" + | "🇸🇩" + | "🇸🇪" + | "🇸🇬" + | "🇸🇭" + | "🇸🇮" + | "🇸🇯" + | "🇸🇰" + | "🇸🇱" + | "🇸🇲" + | "🇸🇳" + | "🇸🇴" + | "🇸🇷" + | "🇸🇸" + | "🇸🇹" + | "🇸🇻" + | "🇸🇽" + | "🇸🇾" + | "🇸🇿" + | "🇹🇦" + | "🇹🇨" + | "🇹🇩" + | "🇹🇫" + | "🇹🇬" + | "🇹🇭" + | "🇹🇯" + | "🇹🇰" + | "🇹🇱" + | "🇹🇲" + | "🇹🇳" + | "🇹🇴" + | "🇹🇷" + | "🇹🇹" + | "🇹🇻" + | "🇹🇼" + | "🇹🇿" + | "🇺🇦" + | "🇺🇬" + | "🇺🇲" + | "🇺🇳" + | "🇺🇸" + | "🇺🇾" + | "🇺🇿" + | "🇻🇦" + | "🇻🇨" + | "🇻🇪" + | "🇻🇬" + | "🇻🇮" + | "🇻🇳" + | "🇻🇺" + | "🇼🇫" + | "🇼🇸" + | "🇽🇰" + | "🇾🇪" + | "🇾🇹" + | "🇿🇦" + | "🇿🇲" + | "🇿🇼" + | "🏴󠁧󠁢󠁥󠁮󠁧󠁿" + | "🏴󠁧󠁢󠁳󠁣󠁴󠁿" + | "🏴󠁧󠁢󠁷󠁬󠁳󠁿" + +export type PageObjectResponse = { + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + properties: Record< + string, + | { type: "number"; number: number | null; id: string } + | { type: "url"; url: string | null; id: string } + | { type: "select"; select: SelectPropertyResponse | null; id: string } + | { + type: "multi_select" + multi_select: Array + id: string + } + | { type: "status"; status: SelectPropertyResponse | null; id: string } + | { type: "date"; date: DateResponse | null; id: string } + | { type: "email"; email: string | null; id: string } + | { type: "phone_number"; phone_number: string | null; id: string } + | { type: "checkbox"; checkbox: boolean; id: string } + | { + type: "files" + files: Array< + | { + file: { url: string; expiry_time: string } + name: StringRequest + type?: "file" + } + | { + external: { url: TextRequest } + name: StringRequest + type?: "external" + } + > + id: string + } + | { + type: "created_by" + created_by: PartialUserObjectResponse | UserObjectResponse + id: string + } + | { type: "created_time"; created_time: string; id: string } + | { + type: "last_edited_by" + last_edited_by: PartialUserObjectResponse | UserObjectResponse + id: string + } + | { type: "last_edited_time"; last_edited_time: string; id: string } + | { type: "formula"; formula: FormulaPropertyResponse; id: string } + | { + type: "unique_id" + unique_id: { prefix: string | null; number: number | null } + id: string + } + | { + type: "verification" + verification: + | VerificationPropertyUnverifiedResponse + | null + | VerificationPropertyResponse + | null + id: string + } + | { type: "title"; title: Array; id: string } + | { type: "rich_text"; rich_text: Array; id: string } + | { + type: "people" + people: Array + id: string + } + | { type: "relation"; relation: Array<{ id: string }>; id: string } + | { + type: "rollup" + rollup: + | { type: "number"; number: number | null; function: RollupFunction } + | { + type: "date" + date: DateResponse | null + function: RollupFunction + } + | { + type: "array" + array: Array< + | { type: "number"; number: number | null } + | { type: "url"; url: string | null } + | { type: "select"; select: SelectPropertyResponse | null } + | { + type: "multi_select" + multi_select: Array + } + | { type: "status"; status: SelectPropertyResponse | null } + | { type: "date"; date: DateResponse | null } + | { type: "email"; email: string | null } + | { type: "phone_number"; phone_number: string | null } + | { type: "checkbox"; checkbox: boolean } + | { + type: "files" + files: Array< + | { + file: { url: string; expiry_time: string } + name: StringRequest + type?: "file" + } + | { + external: { url: TextRequest } + name: StringRequest + type?: "external" + } + > + } + | { + type: "created_by" + created_by: PartialUserObjectResponse | UserObjectResponse + } + | { type: "created_time"; created_time: string } + | { + type: "last_edited_by" + last_edited_by: + | PartialUserObjectResponse + | UserObjectResponse + } + | { type: "last_edited_time"; last_edited_time: string } + | { type: "formula"; formula: FormulaPropertyResponse } + | { + type: "unique_id" + unique_id: { prefix: string | null; number: number | null } + } + | { + type: "verification" + verification: + | VerificationPropertyUnverifiedResponse + | null + | VerificationPropertyResponse + | null + } + | { type: "title"; title: Array } + | { type: "rich_text"; rich_text: Array } + | { + type: "people" + people: Array< + PartialUserObjectResponse | UserObjectResponse + > + } + | { type: "relation"; relation: Array<{ id: string }> } + > + function: RollupFunction + } + id: string + } + > + icon: + | { type: "emoji"; emoji: EmojiRequest } + | null + | { type: "external"; external: { url: TextRequest } } + | null + | { type: "file"; file: { url: string; expiry_time: string } } + | null + cover: + | { type: "external"; external: { url: TextRequest } } + | null + | { type: "file"; file: { url: string; expiry_time: string } } + | null + created_by: PartialUserObjectResponse + last_edited_by: PartialUserObjectResponse + object: "page" + id: string + created_time: string + last_edited_time: string + archived: boolean + url: string + public_url: string | null +} + +export type PartialPageObjectResponse = { object: "page"; id: string } + +type NumberFormat = + | "number" + | "number_with_commas" + | "percent" + | "dollar" + | "canadian_dollar" + | "singapore_dollar" + | "euro" + | "pound" + | "yen" + | "ruble" + | "rupee" + | "won" + | "yuan" + | "real" + | "lira" + | "rupiah" + | "franc" + | "hong_kong_dollar" + | "new_zealand_dollar" + | "krona" + | "norwegian_krone" + | "mexican_peso" + | "rand" + | "new_taiwan_dollar" + | "danish_krone" + | "zloty" + | "baht" + | "forint" + | "koruna" + | "shekel" + | "chilean_peso" + | "philippine_peso" + | "dirham" + | "colombian_peso" + | "riyal" + | "ringgit" + | "leu" + | "argentine_peso" + | "uruguayan_peso" + | "peruvian_sol" + +type NumberDatabasePropertyConfigResponse = { + type: "number" + number: { format: NumberFormat } + id: string + name: string +} + +type FormulaDatabasePropertyConfigResponse = { + type: "formula" + formula: { expression: string } + id: string + name: string +} + +type SelectDatabasePropertyConfigResponse = { + type: "select" + select: { options: Array } + id: string + name: string +} + +type MultiSelectDatabasePropertyConfigResponse = { + type: "multi_select" + multi_select: { options: Array } + id: string + name: string +} + +type StatusPropertyResponse = { + id: StringRequest + name: StringRequest + color: SelectColor +} + +type StatusDatabasePropertyConfigResponse = { + type: "status" + status: { + options: Array + groups: Array<{ + id: StringRequest + name: StringRequest + color: SelectColor + option_ids: Array + }> + } + id: string + name: string +} + +type SinglePropertyDatabasePropertyRelationConfigResponse = { + type: "single_property" + single_property: EmptyObject + database_id: IdRequest +} + +type DualPropertyDatabasePropertyRelationConfigResponse = { + type: "dual_property" + dual_property: { + synced_property_id: StringRequest + synced_property_name: StringRequest + } + database_id: IdRequest +} + +type DatabasePropertyRelationConfigResponse = + | SinglePropertyDatabasePropertyRelationConfigResponse + | DualPropertyDatabasePropertyRelationConfigResponse + +type RelationDatabasePropertyConfigResponse = { + type: "relation" + relation: DatabasePropertyRelationConfigResponse + id: string + name: string +} + +type RollupDatabasePropertyConfigResponse = { + type: "rollup" + rollup: { + rollup_property_name: string + relation_property_name: string + rollup_property_id: string + relation_property_id: string + function: RollupFunction + } + id: string + name: string +} + +type UniqueIdDatabasePropertyConfigResponse = { + type: "unique_id" + unique_id: { prefix: string | null } + id: string + name: string +} + +type TitleDatabasePropertyConfigResponse = { + type: "title" + title: EmptyObject + id: string + name: string +} + +type RichTextDatabasePropertyConfigResponse = { + type: "rich_text" + rich_text: EmptyObject + id: string + name: string +} + +type UrlDatabasePropertyConfigResponse = { + type: "url" + url: EmptyObject + id: string + name: string +} + +type PeopleDatabasePropertyConfigResponse = { + type: "people" + people: EmptyObject + id: string + name: string +} + +type FilesDatabasePropertyConfigResponse = { + type: "files" + files: EmptyObject + id: string + name: string +} + +type EmailDatabasePropertyConfigResponse = { + type: "email" + email: EmptyObject + id: string + name: string +} + +type PhoneNumberDatabasePropertyConfigResponse = { + type: "phone_number" + phone_number: EmptyObject + id: string + name: string +} + +type DateDatabasePropertyConfigResponse = { + type: "date" + date: EmptyObject + id: string + name: string +} + +type CheckboxDatabasePropertyConfigResponse = { + type: "checkbox" + checkbox: EmptyObject + id: string + name: string +} + +type CreatedByDatabasePropertyConfigResponse = { + type: "created_by" + created_by: EmptyObject + id: string + name: string +} + +type CreatedTimeDatabasePropertyConfigResponse = { + type: "created_time" + created_time: EmptyObject + id: string + name: string +} + +type LastEditedByDatabasePropertyConfigResponse = { + type: "last_edited_by" + last_edited_by: EmptyObject + id: string + name: string +} + +type LastEditedTimeDatabasePropertyConfigResponse = { + type: "last_edited_time" + last_edited_time: EmptyObject + id: string + name: string +} + +type DatabasePropertyConfigResponse = + | NumberDatabasePropertyConfigResponse + | FormulaDatabasePropertyConfigResponse + | SelectDatabasePropertyConfigResponse + | MultiSelectDatabasePropertyConfigResponse + | StatusDatabasePropertyConfigResponse + | RelationDatabasePropertyConfigResponse + | RollupDatabasePropertyConfigResponse + | UniqueIdDatabasePropertyConfigResponse + | TitleDatabasePropertyConfigResponse + | RichTextDatabasePropertyConfigResponse + | UrlDatabasePropertyConfigResponse + | PeopleDatabasePropertyConfigResponse + | FilesDatabasePropertyConfigResponse + | EmailDatabasePropertyConfigResponse + | PhoneNumberDatabasePropertyConfigResponse + | DateDatabasePropertyConfigResponse + | CheckboxDatabasePropertyConfigResponse + | CreatedByDatabasePropertyConfigResponse + | CreatedTimeDatabasePropertyConfigResponse + | LastEditedByDatabasePropertyConfigResponse + | LastEditedTimeDatabasePropertyConfigResponse -// TODO: type assertions to verify that each interface is synchronized to the list of keys in the runtime value below. +export type PartialDatabaseObjectResponse = { + object: "database" + id: string + properties: Record +} + +export type DatabaseObjectResponse = { + title: Array + description: Array + icon: + | { type: "emoji"; emoji: EmojiRequest } + | null + | { type: "external"; external: { url: TextRequest } } + | null + | { type: "file"; file: { url: string; expiry_time: string } } + | null + cover: + | { type: "external"; external: { url: TextRequest } } + | null + | { type: "file"; file: { url: string; expiry_time: string } } + | null + properties: Record + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + created_by: PartialUserObjectResponse + last_edited_by: PartialUserObjectResponse + is_inline: boolean + object: "database" + id: string + created_time: string + last_edited_time: string + archived: boolean + url: string + public_url: string | null +} + +export type PartialBlockObjectResponse = { object: "block"; id: string } + +type ApiColor = + | "default" + | "gray" + | "brown" + | "orange" + | "yellow" + | "green" + | "blue" + | "purple" + | "pink" + | "red" + | "gray_background" + | "brown_background" + | "orange_background" + | "yellow_background" + | "green_background" + | "blue_background" + | "purple_background" + | "pink_background" + | "red_background" + +export type ParagraphBlockObjectResponse = { + type: "paragraph" + paragraph: { rich_text: Array; color: ApiColor } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type Heading1BlockObjectResponse = { + type: "heading_1" + heading_1: { + rich_text: Array + color: ApiColor + is_toggleable: boolean + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type Heading2BlockObjectResponse = { + type: "heading_2" + heading_2: { + rich_text: Array + color: ApiColor + is_toggleable: boolean + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type Heading3BlockObjectResponse = { + type: "heading_3" + heading_3: { + rich_text: Array + color: ApiColor + is_toggleable: boolean + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type BulletedListItemBlockObjectResponse = { + type: "bulleted_list_item" + bulleted_list_item: { + rich_text: Array + color: ApiColor + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type NumberedListItemBlockObjectResponse = { + type: "numbered_list_item" + numbered_list_item: { + rich_text: Array + color: ApiColor + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type QuoteBlockObjectResponse = { + type: "quote" + quote: { rich_text: Array; color: ApiColor } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type ToDoBlockObjectResponse = { + type: "to_do" + to_do: { + rich_text: Array + color: ApiColor + checked: boolean + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type ToggleBlockObjectResponse = { + type: "toggle" + toggle: { rich_text: Array; color: ApiColor } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type TemplateBlockObjectResponse = { + type: "template" + template: { rich_text: Array } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type SyncedBlockBlockObjectResponse = { + type: "synced_block" + synced_block: { + synced_from: { type: "block_id"; block_id: IdRequest } | null + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type ChildPageBlockObjectResponse = { + type: "child_page" + child_page: { title: string } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type ChildDatabaseBlockObjectResponse = { + type: "child_database" + child_database: { title: string } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type EquationBlockObjectResponse = { + type: "equation" + equation: { expression: string } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +type LanguageRequest = + | "abap" + | "agda" + | "arduino" + | "assembly" + | "bash" + | "basic" + | "bnf" + | "c" + | "c#" + | "c++" + | "clojure" + | "coffeescript" + | "coq" + | "css" + | "dart" + | "dhall" + | "diff" + | "docker" + | "ebnf" + | "elixir" + | "elm" + | "erlang" + | "f#" + | "flow" + | "fortran" + | "gherkin" + | "glsl" + | "go" + | "graphql" + | "groovy" + | "haskell" + | "html" + | "idris" + | "java" + | "javascript" + | "json" + | "julia" + | "kotlin" + | "latex" + | "less" + | "lisp" + | "livescript" + | "llvm ir" + | "lua" + | "makefile" + | "markdown" + | "markup" + | "matlab" + | "mathematica" + | "mermaid" + | "nix" + | "notion formula" + | "objective-c" + | "ocaml" + | "pascal" + | "perl" + | "php" + | "plain text" + | "powershell" + | "prolog" + | "protobuf" + | "purescript" + | "python" + | "r" + | "racket" + | "reason" + | "ruby" + | "rust" + | "sass" + | "scala" + | "scheme" + | "scss" + | "shell" + | "solidity" + | "sql" + | "swift" + | "toml" + | "typescript" + | "vb.net" + | "verilog" + | "vhdl" + | "visual basic" + | "webassembly" + | "xml" + | "yaml" + | "java/c/c++/c#" + +export type CodeBlockObjectResponse = { + type: "code" + code: { + rich_text: Array + caption: Array + language: LanguageRequest + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} -/* - * databases.retrieve() - */ +export type CalloutBlockObjectResponse = { + type: "callout" + callout: { + rich_text: Array + color: ApiColor + icon: + | { type: "emoji"; emoji: EmojiRequest } + | null + | { type: "external"; external: { url: TextRequest } } + | null + | { type: "file"; file: { url: string; expiry_time: string } } + | null + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type DividerBlockObjectResponse = { + type: "divider" + divider: EmptyObject + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type BreadcrumbBlockObjectResponse = { + type: "breadcrumb" + breadcrumb: EmptyObject + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type TableOfContentsBlockObjectResponse = { + type: "table_of_contents" + table_of_contents: { color: ApiColor } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type ColumnListBlockObjectResponse = { + type: "column_list" + column_list: EmptyObject + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} -interface DatabasesRetrievePathParameters { - database_id: string; +export type ColumnBlockObjectResponse = { + type: "column" + column: EmptyObject + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean } -interface DatabasesRetrieveQueryParameters {} -interface DatabasesRetrieveBodyParameters {} -export interface DatabasesRetrieveParameters extends DatabasesRetrievePathParameters, DatabasesRetrieveQueryParameters, DatabasesRetrieveBodyParameters {} -export interface DatabasesRetrieveResponse extends NotionDatabase {} +export type LinkToPageBlockObjectResponse = { + type: "link_to_page" + link_to_page: + | { type: "page_id"; page_id: IdRequest } + | { type: "database_id"; database_id: IdRequest } + | { type: "comment_id"; comment_id: IdRequest } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type TableBlockObjectResponse = { + type: "table" + table: { + has_column_header: boolean + has_row_header: boolean + table_width: number + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type TableRowBlockObjectResponse = { + type: "table_row" + table_row: { cells: Array> } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type EmbedBlockObjectResponse = { + type: "embed" + embed: { url: string; caption: Array } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type BookmarkBlockObjectResponse = { + type: "bookmark" + bookmark: { url: string; caption: Array } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type ImageBlockObjectResponse = { + type: "image" + image: + | { + type: "external" + external: { url: TextRequest } + caption: Array + } + | { + type: "file" + file: { url: string; expiry_time: string } + caption: Array + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type VideoBlockObjectResponse = { + type: "video" + video: + | { + type: "external" + external: { url: TextRequest } + caption: Array + } + | { + type: "file" + file: { url: string; expiry_time: string } + caption: Array + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type PdfBlockObjectResponse = { + type: "pdf" + pdf: + | { + type: "external" + external: { url: TextRequest } + caption: Array + } + | { + type: "file" + file: { url: string; expiry_time: string } + caption: Array + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type FileBlockObjectResponse = { + type: "file" + file: + | { + type: "external" + external: { url: TextRequest } + caption: Array + } + | { + type: "file" + file: { url: string; expiry_time: string } + caption: Array + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type AudioBlockObjectResponse = { + type: "audio" + audio: + | { + type: "external" + external: { url: TextRequest } + caption: Array + } + | { + type: "file" + file: { url: string; expiry_time: string } + caption: Array + } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type LinkPreviewBlockObjectResponse = { + type: "link_preview" + link_preview: { url: TextRequest } + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type UnsupportedBlockObjectResponse = { + type: "unsupported" + unsupported: EmptyObject + parent: + | { type: "database_id"; database_id: string } + | { type: "page_id"; page_id: string } + | { type: "block_id"; block_id: string } + | { type: "workspace"; workspace: true } + object: "block" + id: string + created_time: string + created_by: PartialUserObjectResponse + last_edited_time: string + last_edited_by: PartialUserObjectResponse + has_children: boolean + archived: boolean +} + +export type BlockObjectResponse = + | ParagraphBlockObjectResponse + | Heading1BlockObjectResponse + | Heading2BlockObjectResponse + | Heading3BlockObjectResponse + | BulletedListItemBlockObjectResponse + | NumberedListItemBlockObjectResponse + | QuoteBlockObjectResponse + | ToDoBlockObjectResponse + | ToggleBlockObjectResponse + | TemplateBlockObjectResponse + | SyncedBlockBlockObjectResponse + | ChildPageBlockObjectResponse + | ChildDatabaseBlockObjectResponse + | EquationBlockObjectResponse + | CodeBlockObjectResponse + | CalloutBlockObjectResponse + | DividerBlockObjectResponse + | BreadcrumbBlockObjectResponse + | TableOfContentsBlockObjectResponse + | ColumnListBlockObjectResponse + | ColumnBlockObjectResponse + | LinkToPageBlockObjectResponse + | TableBlockObjectResponse + | TableRowBlockObjectResponse + | EmbedBlockObjectResponse + | BookmarkBlockObjectResponse + | ImageBlockObjectResponse + | VideoBlockObjectResponse + | PdfBlockObjectResponse + | FileBlockObjectResponse + | AudioBlockObjectResponse + | LinkPreviewBlockObjectResponse + | UnsupportedBlockObjectResponse + +export type NumberPropertyItemObjectResponse = { + type: "number" + number: number | null + object: "property_item" + id: string +} + +export type UrlPropertyItemObjectResponse = { + type: "url" + url: string | null + object: "property_item" + id: string +} + +export type SelectPropertyItemObjectResponse = { + type: "select" + select: SelectPropertyResponse | null + object: "property_item" + id: string +} + +export type MultiSelectPropertyItemObjectResponse = { + type: "multi_select" + multi_select: Array + object: "property_item" + id: string +} + +export type StatusPropertyItemObjectResponse = { + type: "status" + status: SelectPropertyResponse | null + object: "property_item" + id: string +} + +export type DatePropertyItemObjectResponse = { + type: "date" + date: DateResponse | null + object: "property_item" + id: string +} + +export type EmailPropertyItemObjectResponse = { + type: "email" + email: string | null + object: "property_item" + id: string +} + +export type PhoneNumberPropertyItemObjectResponse = { + type: "phone_number" + phone_number: string | null + object: "property_item" + id: string +} + +export type CheckboxPropertyItemObjectResponse = { + type: "checkbox" + checkbox: boolean + object: "property_item" + id: string +} + +export type FilesPropertyItemObjectResponse = { + type: "files" + files: Array< + | { + file: { url: string; expiry_time: string } + name: StringRequest + type?: "file" + } + | { external: { url: TextRequest }; name: StringRequest; type?: "external" } + > + object: "property_item" + id: string +} + +export type CreatedByPropertyItemObjectResponse = { + type: "created_by" + created_by: PartialUserObjectResponse | UserObjectResponse + object: "property_item" + id: string +} + +export type CreatedTimePropertyItemObjectResponse = { + type: "created_time" + created_time: string + object: "property_item" + id: string +} + +export type LastEditedByPropertyItemObjectResponse = { + type: "last_edited_by" + last_edited_by: PartialUserObjectResponse | UserObjectResponse + object: "property_item" + id: string +} -export const databasesRetrieve = { - method: 'get', - // The following lists are synchronized with keyof DatabasesRetrievePathParams / DatabasesRetrieveQueryParameters / DatabasesRetrieveBodyParameters - pathParams: ['database_id'], +export type LastEditedTimePropertyItemObjectResponse = { + type: "last_edited_time" + last_edited_time: string + object: "property_item" + id: string +} + +export type FormulaPropertyItemObjectResponse = { + type: "formula" + formula: FormulaPropertyResponse + object: "property_item" + id: string +} + +export type UniqueIdPropertyItemObjectResponse = { + type: "unique_id" + unique_id: { prefix: string | null; number: number | null } + object: "property_item" + id: string +} + +export type VerificationPropertyItemObjectResponse = { + type: "verification" + verification: + | VerificationPropertyUnverifiedResponse + | null + | VerificationPropertyResponse + | null + object: "property_item" + id: string +} + +export type TitlePropertyItemObjectResponse = { + type: "title" + title: RichTextItemResponse + object: "property_item" + id: string +} + +export type RichTextPropertyItemObjectResponse = { + type: "rich_text" + rich_text: RichTextItemResponse + object: "property_item" + id: string +} + +export type PeoplePropertyItemObjectResponse = { + type: "people" + people: PartialUserObjectResponse | UserObjectResponse + object: "property_item" + id: string +} + +export type RelationPropertyItemObjectResponse = { + type: "relation" + relation: { id: string } + object: "property_item" + id: string +} + +export type RollupPropertyItemObjectResponse = { + type: "rollup" + rollup: + | { type: "number"; number: number | null; function: RollupFunction } + | { type: "date"; date: DateResponse | null; function: RollupFunction } + | { type: "array"; array: Array; function: RollupFunction } + | { + type: "unsupported" + unsupported: EmptyObject + function: RollupFunction + } + | { type: "incomplete"; incomplete: EmptyObject; function: RollupFunction } + object: "property_item" + id: string +} + +export type PropertyItemObjectResponse = + | NumberPropertyItemObjectResponse + | UrlPropertyItemObjectResponse + | SelectPropertyItemObjectResponse + | MultiSelectPropertyItemObjectResponse + | StatusPropertyItemObjectResponse + | DatePropertyItemObjectResponse + | EmailPropertyItemObjectResponse + | PhoneNumberPropertyItemObjectResponse + | CheckboxPropertyItemObjectResponse + | FilesPropertyItemObjectResponse + | CreatedByPropertyItemObjectResponse + | CreatedTimePropertyItemObjectResponse + | LastEditedByPropertyItemObjectResponse + | LastEditedTimePropertyItemObjectResponse + | FormulaPropertyItemObjectResponse + | UniqueIdPropertyItemObjectResponse + | VerificationPropertyItemObjectResponse + | TitlePropertyItemObjectResponse + | RichTextPropertyItemObjectResponse + | PeoplePropertyItemObjectResponse + | RelationPropertyItemObjectResponse + | RollupPropertyItemObjectResponse + +export type CommentObjectResponse = { + object: "comment" + id: string + parent: + | { type: "page_id"; page_id: IdRequest } + | { type: "block_id"; block_id: IdRequest } + discussion_id: string + rich_text: Array + created_by: PartialUserObjectResponse + created_time: string + last_edited_time: string +} + +export type PartialCommentObjectResponse = { object: "comment"; id: string } + +export type PropertyItemPropertyItemListResponse = { + type: "property_item" + property_item: + | { type: "title"; title: EmptyObject; next_url: string | null; id: string } + | { + type: "rich_text" + rich_text: EmptyObject + next_url: string | null + id: string + } + | { + type: "people" + people: EmptyObject + next_url: string | null + id: string + } + | { + type: "relation" + relation: EmptyObject + next_url: string | null + id: string + } + | { + type: "rollup" + rollup: + | { type: "number"; number: number | null; function: RollupFunction } + | { + type: "date" + date: DateResponse | null + function: RollupFunction + } + | { + type: "array" + array: Array + function: RollupFunction + } + | { + type: "unsupported" + unsupported: EmptyObject + function: RollupFunction + } + | { + type: "incomplete" + incomplete: EmptyObject + function: RollupFunction + } + next_url: string | null + id: string + } + object: "list" + next_cursor: string | null + has_more: boolean + results: Array +} + +export type PropertyItemListResponse = PropertyItemPropertyItemListResponse + +type DateRequest = { + start: string + end?: string | null + time_zone?: TimeZoneRequest | null +} + +type TemplateMentionRequest = + | { template_mention_date: "today" | "now"; type?: "template_mention_date" } + | { template_mention_user: "me"; type?: "template_mention_user" } + +type RichTextItemRequest = + | { + text: { content: string; link?: { url: TextRequest } | null } + type?: "text" + annotations?: { + bold?: boolean + italic?: boolean + strikethrough?: boolean + underline?: boolean + code?: boolean + color?: + | "default" + | "gray" + | "brown" + | "orange" + | "yellow" + | "green" + | "blue" + | "purple" + | "pink" + | "red" + | "gray_background" + | "brown_background" + | "orange_background" + | "yellow_background" + | "green_background" + | "blue_background" + | "purple_background" + | "pink_background" + | "red_background" + } + } + | { + mention: + | { + user: + | { id: IdRequest } + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + } + | { date: DateRequest } + | { page: { id: IdRequest } } + | { database: { id: IdRequest } } + | { template_mention: TemplateMentionRequest } + type?: "mention" + annotations?: { + bold?: boolean + italic?: boolean + strikethrough?: boolean + underline?: boolean + code?: boolean + color?: + | "default" + | "gray" + | "brown" + | "orange" + | "yellow" + | "green" + | "blue" + | "purple" + | "pink" + | "red" + | "gray_background" + | "brown_background" + | "orange_background" + | "yellow_background" + | "green_background" + | "blue_background" + | "purple_background" + | "pink_background" + | "red_background" + } + } + | { + equation: { expression: TextRequest } + type?: "equation" + annotations?: { + bold?: boolean + italic?: boolean + strikethrough?: boolean + underline?: boolean + code?: boolean + color?: + | "default" + | "gray" + | "brown" + | "orange" + | "yellow" + | "green" + | "blue" + | "purple" + | "pink" + | "red" + | "gray_background" + | "brown_background" + | "orange_background" + | "yellow_background" + | "green_background" + | "blue_background" + | "purple_background" + | "pink_background" + | "red_background" + } + } + +export type BlockObjectRequestWithoutChildren = + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { equation: { expression: string }; type?: "equation"; object?: "block" } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { rich_text: Array; color?: ApiColor } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { rich_text: Array; color?: ApiColor } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + checked?: boolean + color?: ApiColor + } + type?: "to_do" + object?: "block" + } + | { + toggle: { rich_text: Array; color?: ApiColor } + type?: "toggle" + object?: "block" + } + | { + template: { rich_text: Array } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + color?: ApiColor + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + } + type?: "synced_block" + object?: "block" + } + +export type BlockObjectRequest = + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { equation: { expression: string }; type?: "equation"; object?: "block" } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + column_list: { + children: Array<{ + column: { + children: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { + url: string + caption?: Array + } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { + breadcrumb: EmptyObject + type?: "breadcrumb" + object?: "block" + } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { + block_id: IdRequest + type?: "block_id" + } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "column" + object?: "block" + }> + } + type?: "column_list" + object?: "block" + } + | { + column: { + children: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "column" + object?: "block" + } + | { + table: { + table_width: number + children: Array + has_column_header?: boolean + has_row_header?: boolean + } + type?: "table" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array< + | { + embed: { url: string; caption?: Array } + type?: "embed" + object?: "block" + } + | { + bookmark: { url: string; caption?: Array } + type?: "bookmark" + object?: "block" + } + | { + image: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "image" + object?: "block" + } + | { + video: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "video" + object?: "block" + } + | { + pdf: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "pdf" + object?: "block" + } + | { + file: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "file" + object?: "block" + } + | { + audio: { + external: { url: TextRequest } + type?: "external" + caption?: Array + } + type?: "audio" + object?: "block" + } + | { + code: { + rich_text: Array + language: LanguageRequest + caption?: Array + } + type?: "code" + object?: "block" + } + | { + equation: { expression: string } + type?: "equation" + object?: "block" + } + | { divider: EmptyObject; type?: "divider"; object?: "block" } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; object?: "block" } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + object?: "block" + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + object?: "block" + } + | { + table_row: { cells: Array> } + type?: "table_row" + object?: "block" + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_1" + object?: "block" + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_2" + object?: "block" + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + children?: Array + } + type?: "heading_3" + object?: "block" + } + | { + paragraph: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "paragraph" + object?: "block" + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "bulleted_list_item" + object?: "block" + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "numbered_list_item" + object?: "block" + } + | { + quote: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "quote" + object?: "block" + } + | { + to_do: { + rich_text: Array + color?: ApiColor + children?: Array + checked?: boolean + } + type?: "to_do" + object?: "block" + } + | { + toggle: { + rich_text: Array + color?: ApiColor + children?: Array + } + type?: "toggle" + object?: "block" + } + | { + template: { + rich_text: Array + children?: Array + } + type?: "template" + object?: "block" + } + | { + callout: { + rich_text: Array + color?: ApiColor + children?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + } + type?: "callout" + object?: "block" + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + children?: Array + } + type?: "synced_block" + object?: "block" + } + > + } + type?: "synced_block" + object?: "block" + } + +type ExistencePropertyFilter = { is_empty: true } | { is_not_empty: true } + +type TextPropertyFilter = + | { equals: string } + | { does_not_equal: string } + | { contains: string } + | { does_not_contain: string } + | { starts_with: string } + | { ends_with: string } + | ExistencePropertyFilter + +type NumberPropertyFilter = + | { equals: number } + | { does_not_equal: number } + | { greater_than: number } + | { less_than: number } + | { greater_than_or_equal_to: number } + | { less_than_or_equal_to: number } + | ExistencePropertyFilter + +type CheckboxPropertyFilter = { equals: boolean } | { does_not_equal: boolean } + +type SelectPropertyFilter = + | { equals: string } + | { does_not_equal: string } + | ExistencePropertyFilter + +type MultiSelectPropertyFilter = + | { contains: string } + | { does_not_contain: string } + | ExistencePropertyFilter + +type StatusPropertyFilter = + | { equals: string } + | { does_not_equal: string } + | ExistencePropertyFilter + +type DatePropertyFilter = + | { equals: string } + | { before: string } + | { after: string } + | { on_or_before: string } + | { on_or_after: string } + | { this_week: EmptyObject } + | { past_week: EmptyObject } + | { past_month: EmptyObject } + | { past_year: EmptyObject } + | { next_week: EmptyObject } + | { next_month: EmptyObject } + | { next_year: EmptyObject } + | ExistencePropertyFilter + +type PeoplePropertyFilter = + | { contains: IdRequest } + | { does_not_contain: IdRequest } + | ExistencePropertyFilter + +type RelationPropertyFilter = + | { contains: IdRequest } + | { does_not_contain: IdRequest } + | ExistencePropertyFilter + +type FormulaPropertyFilter = + | { string: TextPropertyFilter } + | { checkbox: CheckboxPropertyFilter } + | { number: NumberPropertyFilter } + | { date: DatePropertyFilter } + +type RollupSubfilterPropertyFilter = + | { rich_text: TextPropertyFilter } + | { number: NumberPropertyFilter } + | { checkbox: CheckboxPropertyFilter } + | { select: SelectPropertyFilter } + | { multi_select: MultiSelectPropertyFilter } + | { relation: RelationPropertyFilter } + | { date: DatePropertyFilter } + | { people: PeoplePropertyFilter } + | { files: ExistencePropertyFilter } + | { status: StatusPropertyFilter } + +type RollupPropertyFilter = + | { any: RollupSubfilterPropertyFilter } + | { none: RollupSubfilterPropertyFilter } + | { every: RollupSubfilterPropertyFilter } + | { date: DatePropertyFilter } + | { number: NumberPropertyFilter } + +type PropertyFilter = + | { title: TextPropertyFilter; property: string; type?: "title" } + | { rich_text: TextPropertyFilter; property: string; type?: "rich_text" } + | { number: NumberPropertyFilter; property: string; type?: "number" } + | { checkbox: CheckboxPropertyFilter; property: string; type?: "checkbox" } + | { select: SelectPropertyFilter; property: string; type?: "select" } + | { + multi_select: MultiSelectPropertyFilter + property: string + type?: "multi_select" + } + | { status: StatusPropertyFilter; property: string; type?: "status" } + | { date: DatePropertyFilter; property: string; type?: "date" } + | { people: PeoplePropertyFilter; property: string; type?: "people" } + | { files: ExistencePropertyFilter; property: string; type?: "files" } + | { url: TextPropertyFilter; property: string; type?: "url" } + | { email: TextPropertyFilter; property: string; type?: "email" } + | { + phone_number: TextPropertyFilter + property: string + type?: "phone_number" + } + | { relation: RelationPropertyFilter; property: string; type?: "relation" } + | { created_by: PeoplePropertyFilter; property: string; type?: "created_by" } + | { + created_time: DatePropertyFilter + property: string + type?: "created_time" + } + | { + last_edited_by: PeoplePropertyFilter + property: string + type?: "last_edited_by" + } + | { + last_edited_time: DatePropertyFilter + property: string + type?: "last_edited_time" + } + | { formula: FormulaPropertyFilter; property: string; type?: "formula" } + | { unique_id: NumberPropertyFilter; property: string; type?: "unique_id" } + | { rollup: RollupPropertyFilter; property: string; type?: "rollup" } + +type TimestampCreatedTimeFilter = { + created_time: DatePropertyFilter + timestamp: "created_time" + type?: "created_time" +} + +type TimestampLastEditedTimeFilter = { + last_edited_time: DatePropertyFilter + timestamp: "last_edited_time" + type?: "last_edited_time" +} +export type GetSelfParameters = Record + +export type GetSelfResponse = UserObjectResponse + +export const getSelf = { + method: "get", + pathParams: [], + queryParams: [], + bodyParams: [], + path: (): string => `users/me`, +} as const + +type GetUserPathParameters = { + user_id: IdRequest +} + +export type GetUserParameters = GetUserPathParameters + +export type GetUserResponse = UserObjectResponse + +export const getUser = { + method: "get", + pathParams: ["user_id"], queryParams: [], bodyParams: [], - path: (p: DatabasesRetrievePathParameters) => `databases/${p.database_id}`, -} as const; + path: (p: GetUserPathParameters): string => `users/${p.user_id}`, +} as const -/* - * databases.query() - */ +type ListUsersQueryParameters = { + start_cursor?: string + page_size?: number +} + +export type ListUsersParameters = ListUsersQueryParameters -interface DatabasesQueryPathParameters { - database_id: string; +export type ListUsersResponse = { + type: "user" + user: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array } -interface DatabasesQueryQueryParameters {} -interface DatabasesQueryBodyParameters { - filter?: NotionDatabaseFilter; - sorts?: NotionDatabaseSort[]; - start_cursor?: string; + +export const listUsers = { + method: "get", + pathParams: [], + queryParams: ["start_cursor", "page_size"], + bodyParams: [], + path: (): string => `users`, +} as const + +type CreatePageBodyParameters = { + parent: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + properties: + | Record< + string, + | { title: Array; type?: "title" } + | { rich_text: Array; type?: "rich_text" } + | { number: number | null; type?: "number" } + | { url: TextRequest | null; type?: "url" } + | { + select: + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + type?: "select" + } + | { + multi_select: Array< + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + > + type?: "multi_select" + } + | { + people: Array< + | { id: IdRequest } + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + > + type?: "people" + } + | { email: StringRequest | null; type?: "email" } + | { phone_number: StringRequest | null; type?: "phone_number" } + | { date: DateRequest | null; type?: "date" } + | { checkbox: boolean; type?: "checkbox" } + | { relation: Array<{ id: IdRequest }>; type?: "relation" } + | { + files: Array< + | { + file: { url: string; expiry_time?: string } + name: StringRequest + type?: "file" + } + | { + external: { url: TextRequest } + name: StringRequest + type?: "external" + } + > + type?: "files" + } + | { + status: + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + type?: "status" + } + > + | Record< + string, + | Array + | Array + | number + | null + | TextRequest + | null + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + | Array< + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + > + | Array< + | { id: IdRequest } + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + > + | StringRequest + | null + | StringRequest + | null + | DateRequest + | null + | boolean + | Array<{ id: IdRequest }> + | Array< + | { + file: { url: string; expiry_time?: string } + name: StringRequest + type?: "file" + } + | { + external: { url: TextRequest } + name: StringRequest + type?: "external" + } + > + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + > + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | null + | { external: { url: TextRequest }; type?: "external" } + | null + cover?: { external: { url: TextRequest }; type?: "external" } | null + content?: Array + children?: Array } -export interface DatabasesQueryParameters extends DatabasesQueryPathParameters, DatabasesQueryQueryParameters, DatabasesQueryBodyParameters {} -export interface DatabasesQueryResponse extends PaginatedList {} +export type CreatePageParameters = CreatePageBodyParameters + +export type CreatePageResponse = PageObjectResponse | PartialPageObjectResponse -export const databasesQuery = { - method: 'post', - // The following lists are synchronized with keyof DatabasesQueryPathParams / DatabasesQueryQueryParams / DatabasesQueryBodyParams - pathParams: ['database_id'], +export const createPage = { + method: "post", + pathParams: [], queryParams: [], - bodyParams: ['filter', 'sorts', 'start_cursor'], - path: (p: DatabasesRetrievePathParameters) => `databases/${p.database_id}`, -} as const; + bodyParams: ["parent", "properties", "icon", "cover", "content", "children"], + path: (): string => `pages`, +} as const + +type GetPagePathParameters = { + page_id: IdRequest +} + +type GetPageQueryParameters = { + filter_properties?: Array +} + +export type GetPageParameters = GetPagePathParameters & GetPageQueryParameters + +export type GetPageResponse = PageObjectResponse | PartialPageObjectResponse +export const getPage = { + method: "get", + pathParams: ["page_id"], + queryParams: ["filter_properties"], + bodyParams: [], + path: (p: GetPagePathParameters): string => `pages/${p.page_id}`, +} as const + +type UpdatePagePathParameters = { + page_id: IdRequest +} + +type UpdatePageBodyParameters = { + properties?: + | Record< + string, + | { title: Array; type?: "title" } + | { rich_text: Array; type?: "rich_text" } + | { number: number | null; type?: "number" } + | { url: TextRequest | null; type?: "url" } + | { + select: + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + type?: "select" + } + | { + multi_select: Array< + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + > + type?: "multi_select" + } + | { + people: Array< + | { id: IdRequest } + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + > + type?: "people" + } + | { email: StringRequest | null; type?: "email" } + | { phone_number: StringRequest | null; type?: "phone_number" } + | { date: DateRequest | null; type?: "date" } + | { checkbox: boolean; type?: "checkbox" } + | { relation: Array<{ id: IdRequest }>; type?: "relation" } + | { + files: Array< + | { + file: { url: string; expiry_time?: string } + name: StringRequest + type?: "file" + } + | { + external: { url: TextRequest } + name: StringRequest + type?: "external" + } + > + type?: "files" + } + | { + status: + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + type?: "status" + } + > + | Record< + string, + | Array + | Array + | number + | null + | TextRequest + | null + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + | Array< + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + > + | Array< + | { id: IdRequest } + | { + person: { email?: string } + id: IdRequest + type?: "person" + name?: string | null + avatar_url?: string | null + object?: "user" + } + | { + bot: + | EmptyObject + | { + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + workspace_name: string | null + } + id: IdRequest + type?: "bot" + name?: string | null + avatar_url?: string | null + object?: "user" + } + > + | StringRequest + | null + | StringRequest + | null + | DateRequest + | null + | boolean + | Array<{ id: IdRequest }> + | Array< + | { + file: { url: string; expiry_time?: string } + name: StringRequest + type?: "file" + } + | { + external: { url: TextRequest } + name: StringRequest + type?: "external" + } + > + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | null + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + | null + > + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | null + | { external: { url: TextRequest }; type?: "external" } + | null + cover?: { external: { url: TextRequest }; type?: "external" } | null + archived?: boolean +} + +export type UpdatePageParameters = UpdatePagePathParameters & + UpdatePageBodyParameters + +export type UpdatePageResponse = PageObjectResponse | PartialPageObjectResponse + +export const updatePage = { + method: "patch", + pathParams: ["page_id"], + queryParams: [], + bodyParams: ["properties", "icon", "cover", "archived"], + path: (p: UpdatePagePathParameters): string => `pages/${p.page_id}`, +} as const + +type GetPagePropertyPathParameters = { + page_id: IdRequest + property_id: string +} + +type GetPagePropertyQueryParameters = { + start_cursor?: string + page_size?: number +} + +export type GetPagePropertyParameters = GetPagePropertyPathParameters & + GetPagePropertyQueryParameters + +export type GetPagePropertyResponse = + | PropertyItemObjectResponse + | PropertyItemListResponse + +export const getPageProperty = { + method: "get", + pathParams: ["page_id", "property_id"], + queryParams: ["start_cursor", "page_size"], + bodyParams: [], + path: (p: GetPagePropertyPathParameters): string => + `pages/${p.page_id}/properties/${p.property_id}`, +} as const + +type GetBlockPathParameters = { + block_id: IdRequest +} + +export type GetBlockParameters = GetBlockPathParameters + +export type GetBlockResponse = PartialBlockObjectResponse | BlockObjectResponse + +export const getBlock = { + method: "get", + pathParams: ["block_id"], + queryParams: [], + bodyParams: [], + path: (p: GetBlockPathParameters): string => `blocks/${p.block_id}`, +} as const + +type UpdateBlockPathParameters = { + block_id: IdRequest +} + +type UpdateBlockBodyParameters = + | { + embed: { url?: string; caption?: Array } + type?: "embed" + archived?: boolean + } + | { + bookmark: { url?: string; caption?: Array } + type?: "bookmark" + archived?: boolean + } + | { + image: { + caption?: Array + external?: { url: TextRequest } + } + type?: "image" + archived?: boolean + } + | { + video: { + caption?: Array + external?: { url: TextRequest } + } + type?: "video" + archived?: boolean + } + | { + pdf: { + caption?: Array + external?: { url: TextRequest } + } + type?: "pdf" + archived?: boolean + } + | { + file: { + caption?: Array + external?: { url: TextRequest } + } + type?: "file" + archived?: boolean + } + | { + audio: { + caption?: Array + external?: { url: TextRequest } + } + type?: "audio" + archived?: boolean + } + | { + code: { + rich_text?: Array + language?: LanguageRequest + caption?: Array + } + type?: "code" + archived?: boolean + } + | { equation: { expression: string }; type?: "equation"; archived?: boolean } + | { divider: EmptyObject; type?: "divider"; archived?: boolean } + | { breadcrumb: EmptyObject; type?: "breadcrumb"; archived?: boolean } + | { + table_of_contents: { color?: ApiColor } + type?: "table_of_contents" + archived?: boolean + } + | { + link_to_page: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + | { comment_id: IdRequest; type?: "comment_id" } + type?: "link_to_page" + archived?: boolean + } + | { + table_row: { cells: Array> } + type?: "table_row" + archived?: boolean + } + | { + heading_1: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + } + type?: "heading_1" + archived?: boolean + } + | { + heading_2: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + } + type?: "heading_2" + archived?: boolean + } + | { + heading_3: { + rich_text: Array + color?: ApiColor + is_toggleable?: boolean + } + type?: "heading_3" + archived?: boolean + } + | { + paragraph: { rich_text: Array; color?: ApiColor } + type?: "paragraph" + archived?: boolean + } + | { + bulleted_list_item: { + rich_text: Array + color?: ApiColor + } + type?: "bulleted_list_item" + archived?: boolean + } + | { + numbered_list_item: { + rich_text: Array + color?: ApiColor + } + type?: "numbered_list_item" + archived?: boolean + } + | { + quote: { rich_text: Array; color?: ApiColor } + type?: "quote" + archived?: boolean + } + | { + to_do: { + rich_text?: Array + checked?: boolean + color?: ApiColor + } + type?: "to_do" + archived?: boolean + } + | { + toggle: { rich_text: Array; color?: ApiColor } + type?: "toggle" + archived?: boolean + } + | { + template: { rich_text: Array } + type?: "template" + archived?: boolean + } + | { + callout: { + rich_text?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | { external: { url: TextRequest }; type?: "external" } + color?: ApiColor + } + type?: "callout" + archived?: boolean + } + | { + synced_block: { + synced_from: { block_id: IdRequest; type?: "block_id" } | null + } + type?: "synced_block" + archived?: boolean + } + | { + table: { has_column_header?: boolean; has_row_header?: boolean } + type?: "table" + archived?: boolean + } + | { archived?: boolean } + +export type UpdateBlockParameters = UpdateBlockPathParameters & + UpdateBlockBodyParameters + +export type UpdateBlockResponse = + | PartialBlockObjectResponse + | BlockObjectResponse + +export const updateBlock = { + method: "patch", + pathParams: ["block_id"], + queryParams: [], + bodyParams: [ + "embed", + "type", + "archived", + "bookmark", + "image", + "video", + "pdf", + "file", + "audio", + "code", + "equation", + "divider", + "breadcrumb", + "table_of_contents", + "link_to_page", + "table_row", + "heading_1", + "heading_2", + "heading_3", + "paragraph", + "bulleted_list_item", + "numbered_list_item", + "quote", + "to_do", + "toggle", + "template", + "callout", + "synced_block", + "table", + ], + path: (p: UpdateBlockPathParameters): string => `blocks/${p.block_id}`, +} as const + +type DeleteBlockPathParameters = { + block_id: IdRequest +} + +export type DeleteBlockParameters = DeleteBlockPathParameters + +export type DeleteBlockResponse = + | PartialBlockObjectResponse + | BlockObjectResponse + +export const deleteBlock = { + method: "delete", + pathParams: ["block_id"], + queryParams: [], + bodyParams: [], + path: (p: DeleteBlockPathParameters): string => `blocks/${p.block_id}`, +} as const + +type ListBlockChildrenPathParameters = { + block_id: IdRequest +} + +type ListBlockChildrenQueryParameters = { + start_cursor?: string + page_size?: number +} + +export type ListBlockChildrenParameters = ListBlockChildrenPathParameters & + ListBlockChildrenQueryParameters + +export type ListBlockChildrenResponse = { + type: "block" + block: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array +} + +export const listBlockChildren = { + method: "get", + pathParams: ["block_id"], + queryParams: ["start_cursor", "page_size"], + bodyParams: [], + path: (p: ListBlockChildrenPathParameters): string => + `blocks/${p.block_id}/children`, +} as const + +type AppendBlockChildrenPathParameters = { + block_id: IdRequest +} + +type AppendBlockChildrenBodyParameters = { + children: Array + after?: IdRequest +} + +export type AppendBlockChildrenParameters = AppendBlockChildrenPathParameters & + AppendBlockChildrenBodyParameters + +export type AppendBlockChildrenResponse = { + type: "block" + block: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array +} + +export const appendBlockChildren = { + method: "patch", + pathParams: ["block_id"], + queryParams: [], + bodyParams: ["children", "after"], + path: (p: AppendBlockChildrenPathParameters): string => + `blocks/${p.block_id}/children`, +} as const + +type GetDatabasePathParameters = { + database_id: IdRequest +} + +export type GetDatabaseParameters = GetDatabasePathParameters + +export type GetDatabaseResponse = + | PartialDatabaseObjectResponse + | DatabaseObjectResponse + +export const getDatabase = { + method: "get", + pathParams: ["database_id"], + queryParams: [], + bodyParams: [], + path: (p: GetDatabasePathParameters): string => `databases/${p.database_id}`, +} as const + +type UpdateDatabasePathParameters = { + database_id: IdRequest +} + +type UpdateDatabaseBodyParameters = { + title?: Array + description?: Array + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | null + | { external: { url: TextRequest }; type?: "external" } + | null + cover?: { external: { url: TextRequest }; type?: "external" } | null + properties?: Record< + string, + | { number: { format?: NumberFormat }; type?: "number"; name?: string } + | null + | { formula: { expression?: string }; type?: "formula"; name?: string } + | null + | { + select: { + options?: Array< + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + > + } + type?: "select" + name?: string + } + | null + | { + multi_select: { + options?: Array< + | { id: StringRequest; name?: StringRequest; color?: SelectColor } + | { name: StringRequest; id?: StringRequest; color?: SelectColor } + > + } + type?: "multi_select" + name?: string + } + | null + | { + relation: + | { + single_property: EmptyObject + database_id: IdRequest + type?: "single_property" + } + | { + dual_property: Record + database_id: IdRequest + type?: "dual_property" + } + type?: "relation" + name?: string + } + | null + | { + rollup: + | { + rollup_property_name: string + relation_property_name: string + function: RollupFunction + rollup_property_id?: string + relation_property_id?: string + } + | { + rollup_property_name: string + relation_property_id: string + function: RollupFunction + relation_property_name?: string + rollup_property_id?: string + } + | { + relation_property_name: string + rollup_property_id: string + function: RollupFunction + rollup_property_name?: string + relation_property_id?: string + } + | { + rollup_property_id: string + relation_property_id: string + function: RollupFunction + rollup_property_name?: string + relation_property_name?: string + } + type?: "rollup" + name?: string + } + | null + | { + unique_id: { prefix?: string | null } + type?: "unique_id" + name?: string + } + | null + | { title: EmptyObject; type?: "title"; name?: string } + | null + | { rich_text: EmptyObject; type?: "rich_text"; name?: string } + | null + | { url: EmptyObject; type?: "url"; name?: string } + | null + | { people: EmptyObject; type?: "people"; name?: string } + | null + | { files: EmptyObject; type?: "files"; name?: string } + | null + | { email: EmptyObject; type?: "email"; name?: string } + | null + | { phone_number: EmptyObject; type?: "phone_number"; name?: string } + | null + | { date: EmptyObject; type?: "date"; name?: string } + | null + | { checkbox: EmptyObject; type?: "checkbox"; name?: string } + | null + | { created_by: EmptyObject; type?: "created_by"; name?: string } + | null + | { created_time: EmptyObject; type?: "created_time"; name?: string } + | null + | { last_edited_by: EmptyObject; type?: "last_edited_by"; name?: string } + | null + | { + last_edited_time: EmptyObject + type?: "last_edited_time" + name?: string + } + | null + | { name: string } + | null + > + is_inline?: boolean + archived?: boolean +} + +export type UpdateDatabaseParameters = UpdateDatabasePathParameters & + UpdateDatabaseBodyParameters + +export type UpdateDatabaseResponse = + | PartialDatabaseObjectResponse + | DatabaseObjectResponse + +export const updateDatabase = { + method: "patch", + pathParams: ["database_id"], + queryParams: [], + bodyParams: [ + "title", + "description", + "icon", + "cover", + "properties", + "is_inline", + "archived", + ], + path: (p: UpdateDatabasePathParameters): string => + `databases/${p.database_id}`, +} as const + +type QueryDatabasePathParameters = { + database_id: IdRequest +} + +type QueryDatabaseQueryParameters = { + filter_properties?: Array +} + +type QueryDatabaseBodyParameters = { + sorts?: Array< + | { property: string; direction: "ascending" | "descending" } + | { + timestamp: "created_time" | "last_edited_time" + direction: "ascending" | "descending" + } + > + filter?: + | { + or: Array< + | PropertyFilter + | TimestampCreatedTimeFilter + | TimestampLastEditedTimeFilter + | { or: Array } + | { and: Array } + > + } + | { + and: Array< + | PropertyFilter + | TimestampCreatedTimeFilter + | TimestampLastEditedTimeFilter + | { or: Array } + | { and: Array } + > + } + | PropertyFilter + | TimestampCreatedTimeFilter + | TimestampLastEditedTimeFilter + start_cursor?: string + page_size?: number + archived?: boolean +} + +export type QueryDatabaseParameters = QueryDatabasePathParameters & + QueryDatabaseQueryParameters & + QueryDatabaseBodyParameters + +export type QueryDatabaseResponse = { + type: "page_or_database" + page_or_database: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array< + | PageObjectResponse + | PartialPageObjectResponse + | PartialDatabaseObjectResponse + | DatabaseObjectResponse + > +} + +export const queryDatabase = { + method: "post", + pathParams: ["database_id"], + queryParams: ["filter_properties"], + bodyParams: ["sorts", "filter", "start_cursor", "page_size", "archived"], + path: (p: QueryDatabasePathParameters): string => + `databases/${p.database_id}/query`, +} as const + +type ListDatabasesQueryParameters = { + start_cursor?: string + page_size?: number +} + +export type ListDatabasesParameters = ListDatabasesQueryParameters + +export type ListDatabasesResponse = { + type: "database" + database: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array +} + +export const listDatabases = { + method: "get", + pathParams: [], + queryParams: ["start_cursor", "page_size"], + bodyParams: [], + path: (): string => `databases`, +} as const + +type CreateDatabaseBodyParameters = { + parent: + | { page_id: IdRequest; type?: "page_id" } + | { database_id: IdRequest; type?: "database_id" } + properties: Record< + string, + | { number: { format?: NumberFormat }; type?: "number" } + | { formula: { expression?: string }; type?: "formula" } + | { + select: { + options?: Array<{ name: StringRequest; color?: SelectColor }> + } + type?: "select" + } + | { + multi_select: { + options?: Array<{ name: StringRequest; color?: SelectColor }> + } + type?: "multi_select" + } + | { + relation: + | { + single_property: EmptyObject + database_id: IdRequest + type?: "single_property" + } + | { + dual_property: Record + database_id: IdRequest + type?: "dual_property" + } + type?: "relation" + } + | { + rollup: + | { + rollup_property_name: string + relation_property_name: string + function: RollupFunction + rollup_property_id?: string + relation_property_id?: string + } + | { + rollup_property_name: string + relation_property_id: string + function: RollupFunction + relation_property_name?: string + rollup_property_id?: string + } + | { + relation_property_name: string + rollup_property_id: string + function: RollupFunction + rollup_property_name?: string + relation_property_id?: string + } + | { + rollup_property_id: string + relation_property_id: string + function: RollupFunction + rollup_property_name?: string + relation_property_name?: string + } + type?: "rollup" + } + | { unique_id: { prefix?: string | null }; type?: "unique_id" } + | { title: EmptyObject; type?: "title" } + | { rich_text: EmptyObject; type?: "rich_text" } + | { url: EmptyObject; type?: "url" } + | { people: EmptyObject; type?: "people" } + | { files: EmptyObject; type?: "files" } + | { email: EmptyObject; type?: "email" } + | { phone_number: EmptyObject; type?: "phone_number" } + | { date: EmptyObject; type?: "date" } + | { checkbox: EmptyObject; type?: "checkbox" } + | { created_by: EmptyObject; type?: "created_by" } + | { created_time: EmptyObject; type?: "created_time" } + | { last_edited_by: EmptyObject; type?: "last_edited_by" } + | { last_edited_time: EmptyObject; type?: "last_edited_time" } + > + icon?: + | { emoji: EmojiRequest; type?: "emoji" } + | null + | { external: { url: TextRequest }; type?: "external" } + | null + cover?: { external: { url: TextRequest }; type?: "external" } | null + title?: Array + description?: Array + is_inline?: boolean +} + +export type CreateDatabaseParameters = CreateDatabaseBodyParameters + +export type CreateDatabaseResponse = + | PartialDatabaseObjectResponse + | DatabaseObjectResponse + +export const createDatabase = { + method: "post", + pathParams: [], + queryParams: [], + bodyParams: [ + "parent", + "properties", + "icon", + "cover", + "title", + "description", + "is_inline", + ], + path: (): string => `databases`, +} as const + +type SearchBodyParameters = { + sort?: { + timestamp: "last_edited_time" + direction: "ascending" | "descending" + } + query?: string + start_cursor?: string + page_size?: number + filter?: { property: "object"; value: "page" | "database" } +} + +export type SearchParameters = SearchBodyParameters + +export type SearchResponse = { + type: "page_or_database" + page_or_database: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array< + | PageObjectResponse + | PartialPageObjectResponse + | PartialDatabaseObjectResponse + | DatabaseObjectResponse + > +} + +export const search = { + method: "post", + pathParams: [], + queryParams: [], + bodyParams: ["sort", "query", "start_cursor", "page_size", "filter"], + path: (): string => `search`, +} as const + +type CreateCommentBodyParameters = + | { + parent: { page_id: IdRequest; type?: "page_id" } + rich_text: Array + } + | { discussion_id: IdRequest; rich_text: Array } + +export type CreateCommentParameters = CreateCommentBodyParameters + +export type CreateCommentResponse = + | CommentObjectResponse + | PartialCommentObjectResponse + +export const createComment = { + method: "post", + pathParams: [], + queryParams: [], + bodyParams: ["parent", "rich_text", "discussion_id"], + path: (): string => `comments`, +} as const + +type ListCommentsQueryParameters = { + block_id: IdRequest + start_cursor?: string + page_size?: number +} + +export type ListCommentsParameters = ListCommentsQueryParameters + +export type ListCommentsResponse = { + type: "comment" + comment: EmptyObject + object: "list" + next_cursor: string | null + has_more: boolean + results: Array +} + +export const listComments = { + method: "get", + pathParams: [], + queryParams: ["block_id", "start_cursor", "page_size"], + bodyParams: [], + path: (): string => `comments`, +} as const + +type OauthTokenBodyParameters = { + grant_type: string + code: string + redirect_uri?: string + external_account?: { key: string; name: string } +} + +export type OauthTokenParameters = OauthTokenBodyParameters + +export type OauthTokenResponse = { + access_token: string + token_type: "bearer" + bot_id: string + workspace_icon: string | null + workspace_name: string | null + workspace_id: string + owner: + | { + type: "user" + user: + | { + type: "person" + person: { email: string } + name: string | null + avatar_url: string | null + id: IdRequest + object: "user" + } + | PartialUserObjectResponse + } + | { type: "workspace"; workspace: true } + duplicated_template_id: string | null +} + +export const oauthToken = { + method: "post", + pathParams: [], + queryParams: [], + bodyParams: ["grant_type", "code", "redirect_uri", "external_account"], + path: (): string => `oauth/token`, +} as const diff --git a/src/api-types.ts b/src/api-types.ts deleted file mode 100644 index 48a6cd9e..00000000 --- a/src/api-types.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Notion API Types - * - * This file contains type definitions for common object types across various interfaces in the Notion API. - * In the future, the contents of this file will be generated from an API definition. - */ - -export type NotionObject = NotionSingularObject | PaginatedList; -export type NotionSingularObject = NotionDatabase | NotionPage; - -interface PropertyFilter { - property: string; - // title?: TextFilter; - // text?: TextFilter; - // number?: NumberFilter; - // checkbox?: CheckboxFilter; - // ... -} - - -// TODO: fill in the rest of these types -export interface NotionDatabase { - object: 'database'; - id: string; -} -export type NotionDatabaseFilter = PropertyFilter; // | ... -export interface NotionDatabaseSort { - // TODO: either property or timestamp are defined but not both - property?: string; - timestamp?: 'created_time' | 'last_edited_time'; - direction: 'ascending' | 'descending'; -} - -export interface NotionPage { - object: 'page', - id: string; -} - -export interface PaginatedList { - object: 'list', - results: O[], - has_more: boolean; - next_cursor: string | null; -} diff --git a/src/errors.ts b/src/errors.ts index 3b55465f..cfec919b 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,159 +1,287 @@ -import type { IncomingHttpHeaders } from 'http'; -import type { - HTTPError as GotHTTPError, - TimeoutError as GotTimeoutError, - Response as GotResponse, -} from 'got'; -import { isObject } from './helpers'; - -export class RequestTimeoutError extends Error { - readonly code = 'notionhq_client_request_timeout'; - - constructor(message = 'Request to Notion API has timed out') { - super(message); - this.name = 'RequestTimeoutError'; - } +import { SupportedResponse } from "./fetch-types" +import { isObject } from "./utils" +import { Assert } from "./type-utils" - static isRequestTimeoutError(error: unknown): error is RequestTimeoutError { - return ( - error instanceof Error && - error.name === 'RequestTimeoutError' && - 'code' in error && error['code'] === RequestTimeoutError.prototype.code - ); - } +/** + * Error codes returned in responses from the API. + */ +export enum APIErrorCode { + Unauthorized = "unauthorized", + RestrictedResource = "restricted_resource", + ObjectNotFound = "object_not_found", + RateLimited = "rate_limited", + InvalidJSON = "invalid_json", + InvalidRequestURL = "invalid_request_url", + InvalidRequest = "invalid_request", + ValidationError = "validation_error", + ConflictError = "conflict_error", + InternalServerError = "internal_server_error", + ServiceUnavailable = "service_unavailable", } -export class HTTPResponseError extends Error { - readonly code: string = 'notionhq_client_response_error'; - readonly status: number; - readonly headers: IncomingHttpHeaders; - readonly body: string; - - constructor(response: GotResponse, message?: string) { - super(message ?? `Request to Notion API failed with status: ${response.statusCode}`); - this.name = 'HTTPResponseError'; - this.status = response.statusCode; - this.headers = response.headers; - this.body = response.rawBody.toString(); - } +/** + * Error codes generated for client errors. + */ +export enum ClientErrorCode { + RequestTimeout = "notionhq_client_request_timeout", + ResponseError = "notionhq_client_response_error", +} - static isHTTPResponseError(error: unknown): error is HTTPResponseError { - return ( - error instanceof Error && - error.name === 'HTTPResponseError' && - 'code' in error && error['code'] === HTTPResponseError.prototype.code - ); - } +/** + * Error codes on errors thrown by the `Client`. + */ +export type NotionErrorCode = APIErrorCode | ClientErrorCode + +/** + * Base error type. + */ +abstract class NotionClientErrorBase< + Code extends NotionErrorCode +> extends Error { + abstract code: Code } +/** + * Error type that encompasses all the kinds of errors that the Notion client will throw. + */ +export type NotionClientError = + | RequestTimeoutError + | UnknownHTTPResponseError + | APIResponseError + +// Assert that NotionClientError's `code` property is a narrow type. +// This prevents us from accidentally regressing to `string`-typed name field. +type _assertCodeIsNarrow = Assert + +// Assert that the type of `name` in NotionErrorCode is a narrow type. +// This prevents us from accidentally regressing to `string`-typed name field. +type _assertNameIsNarrow = Assert< + "RequestTimeoutError" | "UnknownHTTPResponseError" | "APIResponseError", + NotionClientError["name"] +> /** - * Error codes for responses from the API. + * @param error any value, usually a caught error. + * @returns `true` if error is a `NotionClientError`. */ -export enum APIErrorCode { - Unauthorized = 'unauthorized', - RestrictedResource = 'restricted_resource', - ObjectNotFound = 'object_not_found', - RateLimited = 'rate_limited', - InvalidJSON = 'invalid_json', - InvalidRequestURL = 'invalid_request_url', - InvalidRequest = 'invalid_request', - ValidationError = 'validation_error', - ConflictError = 'conflict_error', - InternalServerError = 'internal_server_error', - ServiceUnavailable = 'service_unavailable', +export function isNotionClientError( + error: unknown +): error is NotionClientError { + return isObject(error) && error instanceof NotionClientErrorBase } /** - * Body of an error response from the API. + * Narrows down the types of a NotionClientError. + * @param error any value, usually a caught error. + * @param codes an object mapping from possible error codes to `true` + * @returns `true` if error is a `NotionClientError` with a code in `codes`. */ -interface APIErrorResponseBody { - code: APIErrorCode; - message: string; +function isNotionClientErrorWithCode( + error: unknown, + codes: { [C in Code]: true } +): error is NotionClientError & { code: Code } { + return isNotionClientError(error) && error.code in codes } /** - * A response from the API indicating a problem. - * - * Use the `code` property to handle various kinds of errors. All its possible values are in `APIErrorCode`. + * Error thrown by the client if a request times out. */ -export class APIResponseError extends HTTPResponseError implements APIErrorResponseBody { - readonly code: APIErrorCode; +export class RequestTimeoutError extends NotionClientErrorBase { + readonly code = ClientErrorCode.RequestTimeout + readonly name = "RequestTimeoutError" - constructor(response: GotResponse, body: APIErrorResponseBody) { - super(response, body.message); - this.name = 'APIResponseError'; - this.code = body.code; + constructor(message = "Request to Notion API has timed out") { + super(message) } - static isAPIResponseError(error: unknown): error is APIResponseError { - return ( - error instanceof Error && - error.name === 'APIResponseError' && - 'code' in error && isAPIErrorCode(error['code']) - ); + static isRequestTimeoutError(error: unknown): error is RequestTimeoutError { + return isNotionClientErrorWithCode(error, { + [ClientErrorCode.RequestTimeout]: true, + }) + } + + static rejectAfterTimeout( + promise: Promise, + timeoutMS: number + ): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new RequestTimeoutError()) + }, timeoutMS) + + promise + .then(resolve) + .catch(reject) + .then(() => clearTimeout(timeoutId)) + }) } } +type HTTPResponseErrorCode = ClientErrorCode.ResponseError | APIErrorCode -type RequestError = RequestTimeoutError | HTTPResponseError; +class HTTPResponseError< + Code extends HTTPResponseErrorCode +> extends NotionClientErrorBase { + readonly name: string = "HTTPResponseError" + readonly code: Code + readonly status: number + readonly headers: SupportedResponse["headers"] + readonly body: string -export function buildRequestError(error: unknown): RequestError | undefined { - if (isGotTimeoutError(error)) { - return new RequestTimeoutError(); + constructor(args: { + code: Code + status: number + message: string + headers: SupportedResponse["headers"] + rawBodyText: string + }) { + super(args.message) + const { code, status, headers, rawBodyText } = args + this.code = code + this.status = status + this.headers = headers + this.body = rawBodyText } - if (isGotHTTPError(error)) { - if (isAPIErrorResponseBody(error.response.body)) { - return new APIResponseError(error.response, error.response.body); - } - return new HTTPResponseError(error.response); +} + +const httpResponseErrorCodes: { [C in HTTPResponseErrorCode]: true } = { + [ClientErrorCode.ResponseError]: true, + [APIErrorCode.Unauthorized]: true, + [APIErrorCode.RestrictedResource]: true, + [APIErrorCode.ObjectNotFound]: true, + [APIErrorCode.RateLimited]: true, + [APIErrorCode.InvalidJSON]: true, + [APIErrorCode.InvalidRequestURL]: true, + [APIErrorCode.InvalidRequest]: true, + [APIErrorCode.ValidationError]: true, + [APIErrorCode.ConflictError]: true, + [APIErrorCode.InternalServerError]: true, + [APIErrorCode.ServiceUnavailable]: true, +} + +export function isHTTPResponseError( + error: unknown +): error is UnknownHTTPResponseError | APIResponseError { + if (!isNotionClientErrorWithCode(error, httpResponseErrorCodes)) { + return false } - return; + + type _assert = Assert< + UnknownHTTPResponseError | APIResponseError, + typeof error + > + + return true } -/* - * Type guards +/** + * Error thrown if an API call responds with an unknown error code, or does not respond with + * a property-formatted error. */ +export class UnknownHTTPResponseError extends HTTPResponseError { + readonly name = "UnknownHTTPResponseError" -function isAPIErrorResponseBody(body: unknown): body is APIErrorResponseBody { - if (typeof body !== 'string') { - return false; + constructor(args: { + status: number + message: string | undefined + headers: SupportedResponse["headers"] + rawBodyText: string + }) { + super({ + ...args, + code: ClientErrorCode.ResponseError, + message: + args.message ?? + `Request to Notion API failed with status: ${args.status}`, + }) } - let parsed; - try { - parsed = JSON.parse(body); - } catch (parseError) { - return false; + static isUnknownHTTPResponseError( + error: unknown + ): error is UnknownHTTPResponseError { + return isNotionClientErrorWithCode(error, { + [ClientErrorCode.ResponseError]: true, + }) } +} - return ( - isObject(parsed) && - typeof parsed['message'] === 'string' && - isAPIErrorCode(parsed['code']) - ); +const apiErrorCodes: { [C in APIErrorCode]: true } = { + [APIErrorCode.Unauthorized]: true, + [APIErrorCode.RestrictedResource]: true, + [APIErrorCode.ObjectNotFound]: true, + [APIErrorCode.RateLimited]: true, + [APIErrorCode.InvalidJSON]: true, + [APIErrorCode.InvalidRequestURL]: true, + [APIErrorCode.InvalidRequest]: true, + [APIErrorCode.ValidationError]: true, + [APIErrorCode.ConflictError]: true, + [APIErrorCode.InternalServerError]: true, + [APIErrorCode.ServiceUnavailable]: true, } -function isAPIErrorCode(code: unknown): code is APIErrorCode { - return typeof code === 'string' && Object.values(APIErrorCode).includes(code); +/** + * A response from the API indicating a problem. + * Use the `code` property to handle various kinds of errors. All its possible values are in `APIErrorCode`. + */ +export class APIResponseError extends HTTPResponseError { + readonly name = "APIResponseError" + + static isAPIResponseError(error: unknown): error is APIResponseError { + return isNotionClientErrorWithCode(error, apiErrorCodes) + } +} + +export function buildRequestError( + response: SupportedResponse, + bodyText: string +): APIResponseError | UnknownHTTPResponseError { + const apiErrorResponseBody = parseAPIErrorResponseBody(bodyText) + if (apiErrorResponseBody !== undefined) { + return new APIResponseError({ + code: apiErrorResponseBody.code, + message: apiErrorResponseBody.message, + headers: response.headers, + status: response.status, + rawBodyText: bodyText, + }) + } + return new UnknownHTTPResponseError({ + message: undefined, + headers: response.headers, + status: response.status, + rawBodyText: bodyText, + }) } -function isGotTimeoutError(error: unknown): error is GotTimeoutError { - return ( - error instanceof Error && - error.name === 'TimeoutError' && - 'event' in error && typeof error['event'] === 'string' && - isObject(error['request']) && - isObject(error['timings']) - ); +function parseAPIErrorResponseBody( + body: string +): { code: APIErrorCode; message: string } | undefined { + if (typeof body !== "string") { + return + } + + let parsed: unknown + try { + parsed = JSON.parse(body) + } catch (parseError) { + return + } + + if ( + !isObject(parsed) || + typeof parsed["message"] !== "string" || + !isAPIErrorCode(parsed["code"]) + ) { + return + } + + return { + ...parsed, + code: parsed["code"], + message: parsed["message"], + } } -function isGotHTTPError(error: unknown): error is GotHTTPError { - return ( - error instanceof Error && - error.name === 'HTTPError' && - 'request' in error && isObject(error['request']) && - 'response' in error && isObject(error['response']) && - 'timings' in error && isObject(error['timings']) - ); +function isAPIErrorCode(code: unknown): code is APIErrorCode { + return typeof code === "string" && code in apiErrorCodes } diff --git a/src/fetch-types.ts b/src/fetch-types.ts new file mode 100644 index 00000000..efaf8cfa --- /dev/null +++ b/src/fetch-types.ts @@ -0,0 +1,47 @@ +import type { Agent } from "http" +import type { Assert } from "./type-utils" +import type NodeFetchFn from "node-fetch" +import type { + RequestInfo as NodeFetchRequestInfo, + RequestInit as NodeFetchRequestInit, + Response as NodeFetchResponse, +} from "node-fetch" + +// The `Supported` types should be kept up to date in order to exactly match what we use in the client. This ensures maximal compatibility with other `fetch` implementations. +export type SupportedRequestInfo = string +// We can't assert against the browser or native Node fetch types without complicating the package structure (see #401), so perform a best effort against `node-fetch`, which we use by default. +type _assertSupportedInfoIsSubtypeOfNodeFetch = Assert< + NodeFetchRequestInfo, + SupportedRequestInfo +> + +export type SupportedRequestInit = { + agent?: Agent + body?: string + headers?: Record + method?: string +} +type _assertSupportedInitIsSubtypeOfNodeFetch = Assert< + NodeFetchRequestInit, + SupportedRequestInit +> + +export type SupportedResponse = { + ok: boolean + text: () => Promise + headers: unknown + status: number +} +type _assertSupportedResponseIsSubtypeOfNodeFetch = Assert< + SupportedResponse, + NodeFetchResponse +> + +export type SupportedFetch = ( + url: SupportedRequestInfo, + init?: SupportedRequestInit +) => Promise +type _assertSupportedFetchIsSubtypeOfNodeFetch = Assert< + SupportedFetch, + typeof NodeFetchFn +> diff --git a/src/helpers.ts b/src/helpers.ts index 0c50a4de..38a1f15b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,21 +1,149 @@ +import { + BlockObjectResponse, + CommentObjectResponse, + DatabaseObjectResponse, + PageObjectResponse, + PartialBlockObjectResponse, + PartialCommentObjectResponse, + PartialDatabaseObjectResponse, + PartialPageObjectResponse, + PartialUserObjectResponse, + UserObjectResponse, +} from "./api-endpoints" + +interface PaginatedArgs { + start_cursor?: string +} + +interface PaginatedList { + object: "list" + results: T[] + next_cursor: string | null + has_more: boolean +} /** - * Utility for enforcing exhaustiveness checks in the type system. + * Returns an async iterator over the results of any paginated Notion API. * - * @see https://basarat.gitbook.io/typescript/type-system/discriminated-unions#throw-in-exhaustive-checks + * Example (given a notion Client called `notion`): * - * @param _x The variable with no remaining values + * ``` + * for await (const block of iteratePaginatedAPI(notion.blocks.children.list, { + * block_id: parentBlockId, + * })) { + * // Do something with block. + * } + * ``` + * + * @param listFn A bound function on the Notion client that represents a conforming paginated + * API. Example: `notion.blocks.children.list`. + * @param firstPageArgs Arguments that should be passed to the API on the first and subsequent + * calls to the API. Any necessary `next_cursor` will be automatically populated by + * this function. Example: `{ block_id: "" }` */ -export function assertNever(_x: never): never { // eslint-disable-line @typescript-eslint/no-unused-vars - throw new Error('Unexpected value. Should have been never.'); +export async function* iteratePaginatedAPI( + listFn: (args: Args) => Promise>, + firstPageArgs: Args +): AsyncIterableIterator { + let nextCursor: string | null | undefined = firstPageArgs.start_cursor + do { + const response: PaginatedList = await listFn({ + ...firstPageArgs, + start_cursor: nextCursor, + }) + yield* response.results + nextCursor = response.next_cursor + } while (nextCursor) } +/** + * Collect all of the results of paginating an API into an in-memory array. + * + * Example (given a notion Client called `notion`): + * + * ``` + * const blocks = await collectPaginatedAPI(notion.blocks.children.list, { + * block_id: parentBlockId, + * }) + * // Do something with blocks. + * ``` + * + * @param listFn A bound function on the Notion client that represents a conforming paginated + * API. Example: `notion.blocks.children.list`. + * @param firstPageArgs Arguments that should be passed to the API on the first and subsequent + * calls to the API. Any necessary `next_cursor` will be automatically populated by + * this function. Example: `{ block_id: "" }` + */ +export async function collectPaginatedAPI( + listFn: (args: Args) => Promise>, + firstPageArgs: Args +): Promise { + const results: Item[] = [] + for await (const item of iteratePaginatedAPI(listFn, firstPageArgs)) { + results.push(item) + } + return results +} -export function pick (base: O, keys: readonly K[]): Pick { - const entries = keys.map(key => ([key, base[key]])); - return Object.fromEntries(entries); +/** + * @returns `true` if `response` is a full `BlockObjectResponse`. + */ +export function isFullBlock( + response: BlockObjectResponse | PartialBlockObjectResponse +): response is BlockObjectResponse { + return "type" in response } -export function isObject(o: unknown): o is Record { - return typeof o === 'object' && o !== null; +/** + * @returns `true` if `response` is a full `PageObjectResponse`. + */ +export function isFullPage( + response: PageObjectResponse | PartialPageObjectResponse +): response is PageObjectResponse { + return "url" in response +} + +/** + * @returns `true` if `response` is a full `DatabaseObjectResponse`. + */ +export function isFullDatabase( + response: DatabaseObjectResponse | PartialDatabaseObjectResponse +): response is DatabaseObjectResponse { + return "title" in response +} + +/** + * @returns `true` if `response` is a full `DatabaseObjectResponse` or a full + * `PageObjectResponse`. + */ +export function isFullPageOrDatabase( + response: + | DatabaseObjectResponse + | PartialDatabaseObjectResponse + | PageObjectResponse + | PartialPageObjectResponse +): response is DatabaseObjectResponse | PageObjectResponse { + if (response.object === "database") { + return isFullDatabase(response) + } else { + return isFullPage(response) + } +} + +/** + * @returns `true` if `response` is a full `UserObjectResponse`. + */ +export function isFullUser( + response: UserObjectResponse | PartialUserObjectResponse +): response is UserObjectResponse { + return "type" in response +} + +/** + * @returns `true` if `response` is a full `CommentObjectResponse`. + */ +export function isFullComment( + response: CommentObjectResponse | PartialCommentObjectResponse +): response is CommentObjectResponse { + return "created_by" in response } diff --git a/src/index.ts b/src/index.ts index 3772f2e6..08f1df88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,25 @@ -export { default as Client } from './Client'; -export { APIErrorCode, APIResponseError, HTTPResponseError, RequestTimeoutError } from './errors'; +export { default as Client } from "./Client" +export { LogLevel, Logger } from "./logging" +export { + // Error codes + NotionErrorCode, + APIErrorCode, + ClientErrorCode, + // Error types + NotionClientError, + APIResponseError, + UnknownHTTPResponseError, + RequestTimeoutError, + // Error helpers + isNotionClientError, +} from "./errors" +export { + collectPaginatedAPI, + iteratePaginatedAPI, + isFullBlock, + isFullDatabase, + isFullPage, + isFullUser, + isFullComment, + isFullPageOrDatabase, +} from "./helpers" diff --git a/src/logging.ts b/src/logging.ts index d25c5f4f..8bb06e0b 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,10 +1,20 @@ -import { assertNever } from './helpers'; +import { assertNever } from "./utils" export enum LogLevel { - DEBUG = 'debug', - INFO = 'info', - WARN = 'warn', - ERROR = 'error', + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +export interface Logger { + (level: LogLevel, message: string, extraInfo: Record): void +} + +export function makeConsoleLogger(name: string): Logger { + return (level, message, extraInfo) => { + console[level](`${name} ${level}:`, message, extraInfo) + } } /** @@ -12,10 +22,15 @@ export enum LogLevel { */ export function logLevelSeverity(level: LogLevel): number { switch (level) { - case LogLevel.DEBUG: return 20; - case LogLevel.INFO: return 40; - case LogLevel.WARN: return 60; - case LogLevel.ERROR: return 80; - default: return assertNever(level); + case LogLevel.DEBUG: + return 20 + case LogLevel.INFO: + return 40 + case LogLevel.WARN: + return 60 + case LogLevel.ERROR: + return 80 + default: + return assertNever(level) } } diff --git a/src/type-utils.ts b/src/type-utils.ts new file mode 100644 index 00000000..85568b6a --- /dev/null +++ b/src/type-utils.ts @@ -0,0 +1,8 @@ +/** + * Utilities for working with typescript types + */ + +/** + * Assert U is assignable to T. + */ +export type Assert = U diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..fdd50ac3 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,24 @@ +/** + * Utility for enforcing exhaustiveness checks in the type system. + * + * @see https://basarat.gitbook.io/typescript/type-system/discriminated-unions#throw-in-exhaustive-checks + * + * @param value The variable with no remaining values + */ +export function assertNever(value: never): never { + throw new Error(`Unexpected value should never occur: ${value}`) +} + +type AllKeys = T extends unknown ? keyof T : never + +export function pick>( + base: O, + keys: readonly K[] +): Pick { + const entries = keys.map(key => [key, base?.[key]]) + return Object.fromEntries(entries) +} + +export function isObject(o: unknown): o is Record { + return typeof o === "object" && o !== null +} diff --git a/test/Client.test.ts b/test/Client.test.ts new file mode 100644 index 00000000..487c2153 --- /dev/null +++ b/test/Client.test.ts @@ -0,0 +1,7 @@ +import { Client } from "../src" + +describe("Notion SDK Client", () => { + it("Constructs without throwing", () => { + new Client({ auth: "foo" }) + }) +}) diff --git a/test/client-basics.ts b/test/client-basics.ts deleted file mode 100644 index 5db8bb7a..00000000 --- a/test/client-basics.ts +++ /dev/null @@ -1,7 +0,0 @@ -import test from 'ava'; -import { Client } from '../src'; - -test('initialize client', t => { - new Client({ auth: 'foo' }); - t.pass(); -}); diff --git a/test/helpers.test.ts b/test/helpers.test.ts new file mode 100644 index 00000000..e35c155e --- /dev/null +++ b/test/helpers.test.ts @@ -0,0 +1,60 @@ +import { iteratePaginatedAPI } from "../src/helpers" + +describe("Notion API helpers", () => { + describe(iteratePaginatedAPI, () => { + const mockPaginatedEndpoint = jest.fn< + Promise<{ + object: "list" + results: number[] + next_cursor: string | null + has_more: boolean + }>, + [{ start_cursor?: string }] + >() + + beforeEach(() => { + mockPaginatedEndpoint.mockClear() + }) + + it("Paginates over two pages", async () => { + mockPaginatedEndpoint.mockImplementationOnce(async () => ({ + object: "list", + results: [1, 2], + has_more: true, + next_cursor: "abc", + })) + mockPaginatedEndpoint.mockImplementationOnce(async () => ({ + object: "list", + results: [3, 4], + has_more: false, + next_cursor: null, + })) + const results: number[] = [] + for await (const item of iteratePaginatedAPI(mockPaginatedEndpoint, {})) { + results.push(item) + } + expect(results).toEqual([1, 2, 3, 4]) + expect(mockPaginatedEndpoint).toHaveBeenCalledTimes(2) + expect(mockPaginatedEndpoint.mock.calls[0]?.[0].start_cursor).toBeFalsy() + expect(mockPaginatedEndpoint.mock.calls[1]?.[0].start_cursor).toEqual( + "abc" + ) + }) + + it("Works when there's only one page", async () => { + mockPaginatedEndpoint.mockImplementationOnce(async () => ({ + object: "list", + results: [1, 2], + has_more: false, + next_cursor: null, + })) + const results: number[] = [] + for await (const item of iteratePaginatedAPI(mockPaginatedEndpoint, {})) { + results.push(item) + } + expect(results).toEqual([1, 2]) + expect(mockPaginatedEndpoint).toHaveBeenCalledTimes(1) + expect(mockPaginatedEndpoint.mock.calls[0]?.[0].start_cursor).toBeFalsy() + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 87e5b593..2de515cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,18 +25,18 @@ // Strict mode "strict": true, + // Allow import package.json + "resolveJsonModule": true, + // Linter style rules - "noUnusedLocals": true, + "noUnusedLocals": false, // Disabled because we use eslint for this. "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noPropertyAccessFromIndexSignature": true, - "forceConsistentCasingInFileNames": true, + "forceConsistentCasingInFileNames": true }, - "include": [ - "src/**/*", - "test/**/*", - ], + "include": ["src/**/*", "test/**/*"] }