Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .editorconfig

This file was deleted.

39 changes: 35 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@
dist
node_modules
worker
wrangler.toml
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
11 changes: 0 additions & 11 deletions .prettierrc

This file was deleted.

7 changes: 0 additions & 7 deletions LICENSE.md

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
A **serverless wrapper** for the private Notion API. It provides fast and easy access to your Notion content.
Ideal to make Notion your CMS.

We provide a hosted version of this project on [`https://notion-api.splitbee.io`](https://notion-api.splitbee.io/). You can also [host it yourself](https://workers.cloudflare.com/). Cloudflare offers a generous free plan with up to 100,000 request per day.
We provide a hosted version of this project on [`https://notion-api.splitbee.io`](https://notion-api.splitbee.io/). You can also [host it yourself on Vercel](https://vercel.com/?ref=notion-api-worker). Vercel offers a generous free plan.

_Use with caution. This is based on the private Notion API. We can not gurantee it will stay stable._

Expand Down Expand Up @@ -49,7 +49,7 @@ Example ([Source Notion Page](https://www.notion.so/splitbee/20720198ca7a4e1b92a

All public pages can be accessed without authorization. If you want to fetch private pages there are two options.

- The recommended way is to host your own worker with the `NOTION_TOKEN` environment variable set. You can find more information in the [Cloudflare Workers documentation](https://developers.cloudflare.com/workers/reference/apis/environment-variables/).
- The recommended way is to host your own instance with the `NOTION_TOKEN` environment variable set. You can find more information in the [Vercel environment variables documentation](https://vercel.com/docs/environment-variables).
- Alternatively you can set the `Authorization: Bearer <NOTION_TOKEN>` header to authorize your requests.

### Receiving the token
Expand Down
27 changes: 27 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "notion-api-worker",
"dependencies": {
"hono": "^4.10.0",
},
"devDependencies": {
"@types/node": "^24.8.1",
"prettier": "^3.3.3",
"typescript": "^5.9.3",
},
},
},
"packages": {
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],

"hono": ["[email protected]", "", {}, "sha512-V/S2IyKL6fk5+bEjiQzg74r5BglqAwU20IX3WjdTUFgvmtSqAZjSxN/Zb5lr6/JXVmH0aqkqOq++3UgzOi9+4Q=="],

"prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],

"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],

"undici-types": ["[email protected]", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
}
}
26 changes: 8 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
{
"name": "notion-api-worker",
"version": "0.1.0",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"build": "webpack",
"dev": "wrangler dev",
"preview": "wrangler preview",
"deploy": "wrangler publish -e production"
},
"private": true,
"type": "module",
"dependencies": {
"tiny-request-router": "^1.2.2"
"hono": "^4.10.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^2.0.0",
"@cloudflare/wrangler": "^1.10.3",
"@types/node": "^14.0.24",
"prettier": "^2.0.5",
"ts-loader": "^8.0.1",
"typescript": "^3.9.7",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12"
}
"@types/node": "^24.8.1",
"prettier": "^3.3.3",
"typescript": "^5.9.3"
},
"packageManager": "bun"
}
16 changes: 0 additions & 16 deletions src/get-cache-key.ts

This file was deleted.

101 changes: 11 additions & 90 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,15 @@
import {} from "@cloudflare/workers-types";
import { Router, Method } from "tiny-request-router";
import { Hono } from "hono";

import { pageRoute } from "./routes/page";
import { tableRoute } from "./routes/table";
import { userRoute } from "./routes/user";
import { searchRoute } from "./routes/search";
import { createResponse } from "./response";
import { getCacheKey } from "./get-cache-key";
import * as types from "./api/types";
import { pageRoute } from "./routes/notion-page.js";
import { tableRoute } from "./routes/table.js";
import { userRoute } from "./routes/user.js";
import { searchRoute } from "./routes/search.js";

export type Handler = (
req: types.HandlerRequest
) => Promise<Response> | Response;
const app = new Hono().basePath("/v1");

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
};
app.get("/page/:pageId", pageRoute);
app.get("/table/:pageId", tableRoute);
app.get("/user/:userId", userRoute);
app.get("/search", searchRoute);

const router = new Router<Handler>();

router.options("*", () => new Response(null, { headers: corsHeaders }));
router.get("/v1/page/:pageId", pageRoute);
router.get("/v1/table/:pageId", tableRoute);
router.get("/v1/user/:userId", userRoute);
router.get("/v1/search", searchRoute);

router.get("*", async () =>
createResponse(
{
error: `Route not found!`,
routes: ["/v1/page/:pageId", "/v1/table/:pageId", "/v1/user/:pageId"],
},
{},
404
)
);

const cache = (caches as any).default;
const NOTION_API_TOKEN =
typeof NOTION_TOKEN !== "undefined" ? NOTION_TOKEN : undefined;

const handleRequest = async (fetchEvent: FetchEvent): Promise<Response> => {
const request = fetchEvent.request;
const { pathname, searchParams } = new URL(request.url);
const notionToken =
NOTION_API_TOKEN ||
(request.headers.get("Authorization") || "").split("Bearer ")[1] ||
undefined;

const match = router.match(request.method as Method, pathname);

if (!match) {
return new Response("Endpoint not found.", { status: 404 });
}

const cacheKey = getCacheKey(request);
let response;

if (cacheKey) {
try {
response = await cache.match(cacheKey);
} catch (err) {}
}

const getResponseAndPersist = async () => {
const res = await match.handler({
request,
searchParams,
params: match.params,
notionToken,
});

if (cacheKey) {
await cache.put(cacheKey, res.clone());
}

return res;
};

if (response) {
fetchEvent.waitUntil(getResponseAndPersist());
return response;
}

return getResponseAndPersist();
};

self.addEventListener("fetch", async (event: Event) => {
const fetchEvent = event as FetchEvent;
fetchEvent.respondWith(handleRequest(fetchEvent));
});
export default app;
16 changes: 13 additions & 3 deletions src/api/notion.ts → src/notion-api/notion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
CollectionData,
NotionSearchParamsType,
NotionSearchResultsType,
} from "./types";
} from "./types.js";

const NOTION_API = "https://www.notion.so/api/v3";

interface INotionParams {
resource: string;
body: JSONData;
notionToken?: string;
headers?: Record<string, string>;
}

const loadPageChunkBody = {
Expand All @@ -26,17 +27,19 @@ const fetchNotionData = async <T extends any>({
resource,
body,
notionToken,
headers,
}: INotionParams): Promise<T> => {
const res = await fetch(`${NOTION_API}/${resource}`, {
method: "POST",
headers: {
"content-type": "application/json",
...(notionToken && { cookie: `token_v2=${notionToken}` }),
...headers,
},
body: JSON.stringify(body),
});

return res.json();
return res.json() as Promise<T>;
};

export const fetchPageById = async (pageId: string, notionToken?: string) => {
Expand Down Expand Up @@ -77,8 +80,14 @@ const queryCollectionBody = {
export const fetchTableData = async (
collectionId: string,
collectionViewId: string,
notionToken?: string
notionToken?: string,
spaceId?: string
) => {
const headers: Record<string, string> = {};
if (spaceId) {
headers["x-notion-space-id"] = spaceId;
}

const table = await fetchNotionData<CollectionData>({
resource: "queryCollection",
body: {
Expand All @@ -91,6 +100,7 @@ export const fetchTableData = async (
...queryCollectionBody,
},
notionToken,
headers,
});

return table;
Expand Down
11 changes: 3 additions & 8 deletions src/api/types.ts → src/notion-api/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Params } from "tiny-request-router";
import { Context } from "hono";

type BoldFormatType = ["b"];
type ItalicFormatType = ["i"];
Expand All @@ -11,7 +11,7 @@ type DateFormatType = [
type: "date";
start_date: string;
date_format: string;
}
},
];
type UserFormatType = ["u", string];
type PageFormatType = ["p", string];
Expand Down Expand Up @@ -201,9 +201,4 @@ export interface NotionSearchResultsType {
total: number;
}

export interface HandlerRequest {
params: Params;
searchParams: URLSearchParams;
request: Request;
notionToken?: string;
}
export type HandlerRequest = Context;
8 changes: 1 addition & 7 deletions src/api/utils.ts → src/notion-api/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
DecorationType,
ColumnType,
RowContentType,
BlockType,
RowType,
} from "./types";
import { DecorationType, ColumnType, RowContentType, RowType } from "./types.js";

export const idToUuid = (path: string): string =>
`${path.slice(0, 8)}-${path.slice(8, 12)}-${path.slice(12, 16)}-${path.slice(16, 20)}-${path.slice(20)}`;
Expand Down
17 changes: 0 additions & 17 deletions src/response.ts

This file was deleted.

Loading