This commit is contained in:
Tobias Lins
2020-06-28 16:23:07 +02:00
8 changed files with 168 additions and 37 deletions

View File

@@ -4,6 +4,8 @@ import {
NotionUserType,
LoadPageChunkData,
CollectionData,
NotionSearchParamsType,
NotionSearchResultsType,
} from "./types";
const NOTION_API = "https://www.notion.so/api/v3";
@@ -120,3 +122,34 @@ export const fetchBlocks = async (
notionToken,
});
};
export const fetchNotionSearch = async (
params: NotionSearchParamsType,
notionToken?: string
) => {
// TODO: support other types of searches
return fetchNotionData<{ results: NotionSearchResultsType }>({
resource: "search",
body: {
type: "BlocksInAncestor",
source: "quick_find_public",
ancestorId: params.ancestorId,
filters: {
isDeletedOnly: false,
excludeTemplates: true,
isNavigableOnly: true,
requireEditPermissions: false,
ancestors: [],
createdBy: [],
editedBy: [],
lastEditedTime: {},
createdTime: {},
...params.filters,
},
sort: "Relevance",
limit: params.limit || 20,
query: params.query,
},
notionToken,
});
};

View File

@@ -1,3 +1,5 @@
import { Params } from "tiny-request-router";
type BoldFormatType = ["b"];
type ItalicFormatType = ["i"];
type StrikeFormatType = ["s"];
@@ -146,3 +148,40 @@ export interface CollectionData {
blockIds: string[];
};
}
export interface NotionSearchParamsType {
ancestorId: string;
query: string;
filters?: {
isDeletedOnly: boolean;
excludeTemplates: boolean;
isNavigableOnly: boolean;
requireEditPermissions: boolean;
};
limit?: number;
}
export interface NotionSearchResultType {
id: string;
isNavigable: boolean;
score: number;
highlight: {
pathText: string;
text: string;
};
}
export interface NotionSearchResultsType {
recordMap: {
block: { [key: string]: RowType };
};
results: NotionSearchResultType[];
total: number;
}
export interface HandlerRequest = {
params: Params;
searchParams: URLSearchParams;
request: Request;
notionToken?: string;
}

View File

@@ -7,8 +7,10 @@ export const idToUuid = (path: string) =>
)}-${path.substr(16, 4)}-${path.substr(20)}`;
export const parsePageId = (id: string) => {
const rawId = id.replace(/\-/g, "").slice(-32);
return idToUuid(rawId);
if (id) {
const rawId = id.replace(/\-/g, "").slice(-32);
return idToUuid(rawId);
}
};
export const getNotionValue = (
@@ -20,7 +22,7 @@ export const getNotionValue = (
return getTextContent(val);
case "person":
return (
val.filter(v => v.length > 1).map(v => v[1]![0][1] as string) || []
val.filter((v) => v.length > 1).map((v) => v[1]![0][1] as string) || []
);
case "checkbox":
return val[0][0] === "Yes";

View File

@@ -4,19 +4,27 @@ import { Router, Method, Params } from "tiny-request-router";
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 * as types from "./api/types";
export type Handler = (
params: Params,
notionToken?: string
req: types.HandlerRequest
) => Promise<Response> | Response;
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
};
const router = new Router<Handler>();
router.options("*", () => new Response("", { headers: {} }));
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(
@@ -35,7 +43,7 @@ const NOTION_API_TOKEN =
const handleRequest = async (fetchEvent: FetchEvent): Promise<Response> => {
const request = fetchEvent.request;
const { pathname } = new URL(request.url);
const { pathname, searchParams } = new URL(request.url);
const notionToken =
NOTION_API_TOKEN ||
(request.headers.get("Authorization") || "").split("Bearer ")[1] ||
@@ -57,7 +65,12 @@ const handleRequest = async (fetchEvent: FetchEvent): Promise<Response> => {
} catch (err) {}
const getResponseAndPersist = async () => {
const res = await match.handler(match.params, notionToken);
const res = await match.handler({
request,
searchParams,
params: match.params,
notionToken,
});
await cache.put(cacheKey, res.clone());
return res;

View File

@@ -3,30 +3,39 @@ import { fetchPageById, fetchBlocks } from "../api/notion";
import { parsePageId } from "../api/utils";
import { createResponse } from "../response";
import { getTableData } from "./table";
import { CollectionType, BlockType } from "../api/types";
import { CollectionType, BlockType, HandlerRequest } from "../api/types";
export async function pageRoute(params: Params, notionToken?: string) {
const pageId = parsePageId(params.pageId);
const page = await fetchPageById(pageId, notionToken);
export async function pageRoute(req: HandlerRequest) {
const pageId = parsePageId(req.params.pageId);
const page = await fetchPageById(pageId, req.notionToken);
const baseBlocks = page.recordMap.block;
const baseBlockKeys = Object.keys(baseBlocks);
const pendingBlocks = baseBlockKeys.flatMap((blockId) => {
const block = baseBlocks[blockId];
const content = block.value.content;
return content ? content.filter((id: string) => !baseBlocks[id]) : [];
});
const additionalBlocks = await fetchBlocks(pendingBlocks).then(
(res) => res.recordMap.block
);
const allBlocks: { [id: string]: BlockType & { data?: any } } = {
let allBlocks: { [id: string]: BlockType & { data?: any } } = {
...baseBlocks,
...additionalBlocks
};
let allBlockKeys;
while (true) {
allBlockKeys = Object.keys(allBlocks);
const pendingBlocks = allBlockKeys.flatMap((blockId) => {
const block = allBlocks[blockId];
const content = block.value.content;
return content ? content.filter((id: string) => !allBlocks[id]) : [];
});
if (!pendingBlocks.length) {
break;
}
const newBlocks = await fetchBlocks(pendingBlocks).then(
(res) => res.recordMap.block
);
allBlocks = { ...allBlocks, ...newBlocks };
}
const collection = page.recordMap.collection
? page.recordMap.collection[Object.keys(page.recordMap.collection)[0]]
@@ -41,8 +50,8 @@ export async function pageRoute(params: Params, notionToken?: string) {
: null;
if (collection && collectionView) {
const pendingCollections = baseBlockKeys.flatMap((blockId) => {
const block = baseBlocks[blockId];
const pendingCollections = allBlockKeys.flatMap((blockId) => {
const block = allBlocks[blockId];
return block.value.type === "collection_view" ? [block.value.id] : [];
});
@@ -51,12 +60,12 @@ export async function pageRoute(params: Params, notionToken?: string) {
const data = await getTableData(
collection,
collectionView.value.id,
notionToken
req.notionToken
);
allBlocks[b] = {
...allBlocks[b],
data
data,
};
}
}

29
src/routes/search.ts Normal file
View File

@@ -0,0 +1,29 @@
import { fetchNotionSearch } from "../api/notion";
import { createResponse } from "../response";
import { HandlerRequest } from "../api/types";
import { parsePageId } from "../api/utils";
export async function searchRoute(req: HandlerRequest) {
const ancestorId = parsePageId(req.searchParams.get("ancestorId"));
const query = req.searchParams.get("query") || "";
const limit = req.searchParams.get("limit") || 20;
if (!ancestorId) {
return createResponse(
{ error: 'missing required "ancestorId"' },
{ "Content-Type": "application/json" },
400
);
}
const results = await fetchNotionSearch(
{
ancestorId,
query,
limit,
},
req.notionToken
);
return createResponse(results);
}

View File

@@ -1,7 +1,12 @@
import { Params } from "tiny-request-router";
import { fetchPageById, fetchTableData, fetchNotionUsers } from "../api/notion";
import { parsePageId, getNotionValue } from "../api/utils";
import { RowContentType, CollectionType, RowType } from "../api/types";
import {
RowContentType,
CollectionType,
RowType,
HandlerRequest,
} from "../api/types";
import { createResponse } from "../response";
export const getTableData = async (
@@ -51,9 +56,9 @@ export const getTableData = async (
return rows;
};
export async function tableRoute(params: Params, notionToken?: string) {
const pageId = parsePageId(params.pageId);
const page = await fetchPageById(pageId, notionToken);
export async function tableRoute(req: HandlerRequest) {
const pageId = parsePageId(req.params.pageId);
const page = await fetchPageById(pageId, req.notionToken);
if (!page.recordMap.collection)
return createResponse(
@@ -75,7 +80,7 @@ export async function tableRoute(params: Params, notionToken?: string) {
const rows = await getTableData(
collection,
collectionView.value.id,
notionToken
req.notionToken
);
return createResponse(rows);

View File

@@ -1,9 +1,10 @@
import { Params } from "tiny-request-router";
import { fetchNotionUsers } from "../api/notion";
import { HandlerRequest } from "../api/types";
import { createResponse } from "../response";
export async function userRoute(params: Params, notionToken?: string) {
const users = await fetchNotionUsers([params.userId], notionToken);
export async function userRoute(req: HandlerRequest) {
const users = await fetchNotionUsers([req.params.userId], req.notionToken);
return createResponse(users[0]);
}