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
+
+
+
+## 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
+
+
+
+## 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
+
+
+
+## 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.
+
+
+
+### 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..a2540bdd
--- /dev/null
+++ b/examples/parse-text-from-any-block-type/index.js
@@ -0,0 +1,154 @@
+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 "rich_text":
+ text = getPlainTextFromRichText(block.rich_text)
+ 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. :)
+