Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ yarn-error.log*

# typescript
*.tsbuildinfo

# wrangler
wrangler.toml
66 changes: 66 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion src/notion-api/notion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
NotionSearchParamsType,
NotionSearchResultsType,
} from "./types.js";
import { normalizeRecordMap } from "./utils.js";

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

Expand Down Expand Up @@ -52,6 +53,10 @@ export const fetchPageById = async (pageId: string, notionToken?: string) => {
notionToken,
});

if (res?.recordMap) {
res.recordMap = normalizeRecordMap(res.recordMap);
}

return res;
};

Expand Down Expand Up @@ -93,16 +98,22 @@ export const fetchTableData = async (
body: {
collection: {
id: collectionId,
...(spaceId && { spaceId }),
},
collectionView: {
id: collectionViewId,
...(spaceId && { spaceId }),
},
...queryCollectionBody,
},
notionToken,
headers,
});

if (table?.recordMap) {
table.recordMap = normalizeRecordMap(table.recordMap);
}

return table;
};

Expand Down Expand Up @@ -136,7 +147,7 @@ export const fetchBlocks = async (
blockList: string[],
notionToken?: string
) => {
return await fetchNotionData<LoadPageChunkData>({
const res = await fetchNotionData<LoadPageChunkData>({
resource: "syncRecordValues",
body: {
requests: blockList.map((id) => ({
Expand All @@ -147,6 +158,12 @@ export const fetchBlocks = async (
},
notionToken,
});

if (res?.recordMap) {
res.recordMap = normalizeRecordMap(res.recordMap);
}

return res;
};

export const fetchNotionSearch = async (
Expand Down
53 changes: 53 additions & 0 deletions src/notion-api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,59 @@ export const parsePageId = (id: string) => {
}
};

/**
* Notion now returns records as `{ spaceId, value: { value: T, role } }`.
* Older clients expect `{ role, value: T }`. Normalize to the legacy shape.
*/
export const unwrapRecord = <T extends { role?: string; value?: any; spaceId?: string }>(
record: T | undefined | null
): T | undefined | null => {
if (!record || typeof record !== "object") return record;

const outer = record as any;
const nested = outer.value;

// New format: { spaceId?, value: { value: actual, role } }
if (
nested &&
typeof nested === "object" &&
nested.value &&
typeof nested.value === "object" &&
(nested.value.id || nested.value.schema || nested.value.type)
) {
return {
...outer,
role: nested.role ?? outer.role,
value: nested.value,
} as T;
}

return record;
};

export const normalizeRecordMap = <T extends Record<string, any> | undefined | null>(
recordMap: T
): T => {
if (!recordMap || typeof recordMap !== "object") return recordMap;

const normalized: Record<string, any> = { ...recordMap };

for (const table of Object.keys(normalized)) {
const entries = normalized[table];
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
// Skip metadata keys like __version__
if (table.startsWith("__")) continue;

const next: Record<string, any> = {};
for (const [id, record] of Object.entries(entries)) {
next[id] = unwrapRecord(record as any) ?? record;
}
normalized[table] = next;
}

return normalized as T;
};

export const getNotionValue = (
val: DecorationType[],
type: ColumnType,
Expand Down
10 changes: 7 additions & 3 deletions src/routes/notion-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,29 @@ export async function pageRoute(c: HandlerRequest) {
if (collection && collectionView) {
const pendingCollections = allBlockKeys.flatMap((blockId) => {
const block = allBlocks[blockId];
const type = block.value && block.value.type;

return block.value && block.value.type === "collection_view"
// collection_view_page is a full-page database; collection_view is inline
return type === "collection_view" || type === "collection_view_page"
? [block.value.id]
: [];
});

for (let b of pendingCollections) {
const collPage = await fetchPageById(b!, notionToken);

const coll = Object.keys(collPage.recordMap.collection).map(
const coll = Object.keys(collPage.recordMap.collection || {}).map(
(k) => collPage.recordMap.collection[k]
)[0];

const collView: {
value: { id: CollectionType["value"]["id"] };
} = Object.keys(collPage.recordMap.collection_view).map(
} = Object.keys(collPage.recordMap.collection_view || {}).map(
(k) => collPage.recordMap.collection_view[k]
)[0];

if (!coll || !collView) continue;

const { rows, schema } = await getTableData(
coll,
collView.value.id,
Expand Down
26 changes: 19 additions & 7 deletions src/routes/table.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
import {
fetchPageById,
fetchTableData,
Expand All @@ -19,23 +20,34 @@ export const getTableData = async (
notionToken?: string,
raw?: boolean
) => {
const spaceId =
(collection as any).spaceId ||
(collection.value as any)?.space_id ||
undefined;

const table = await fetchTableData(
collection.value.id,
collectionViewId,
notionToken
notionToken,
spaceId
);

const collectionRows = collection.value.schema;
const collectionColKeys = Object.keys(collectionRows);
const collectionColKeys = Object.keys(collectionRows || {});

const tableArr: RowType[] =
table.result.reducerResults.collection_group_results.blockIds.map(
(id: string) => table.recordMap.block[id]
);
const blockIds =
table?.result?.reducerResults?.collection_group_results?.blockIds || [];

const tableArr: RowType[] = blockIds.map(
(id: string) => table.recordMap.block[id]
);

const tableData = tableArr.filter(
(b) =>
b.value && b.value.properties && b.value.parent_id === collection.value.id
b &&
b.value &&
b.value.properties &&
b.value.parent_id === collection.value.id
);

type Row = { id: string; [key: string]: RowContentType };
Expand Down
10 changes: 5 additions & 5 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { HandlerRequest } from "../notion-api/types.js";

export const getNotionToken = (c: HandlerRequest) => {
return (
process.env.NOTION_TOKEN ||
(c.req.header("Authorization") || "").split("Bearer ")[1] ||
undefined
);
const fromContext = (c.env as { NOTION_TOKEN?: string } | undefined)?.NOTION_TOKEN;
const fromProcess =
typeof process !== "undefined" ? process.env?.NOTION_TOKEN : undefined;
const fromHeader = (c.req.header("Authorization") || "").split("Bearer ")[1];
return fromContext || fromProcess || fromHeader || undefined;
};