From 5056ed1af133957acd2c8ffa1b6a9654b0d7e50d Mon Sep 17 00:00:00 2001 From: Trevor Healy Date: Wed, 8 Oct 2025 09:53:55 -0700 Subject: [PATCH 1/2] Query Collection Requires Specific Base URL --- src/api/notion.ts | 45 +++++++++++++++++++++++++++++++++------------ src/api/types.ts | 4 ++++ src/routes/page.ts | 2 ++ src/routes/table.ts | 11 +++++++++-- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/api/notion.ts b/src/api/notion.ts index f3da329..d107eba 100644 --- a/src/api/notion.ts +++ b/src/api/notion.ts @@ -5,6 +5,7 @@ import { CollectionData, NotionSearchParamsType, NotionSearchResultsType, + BlockType, } from "./types"; const NOTION_API = "https://www.notion.so/api/v3"; @@ -13,6 +14,7 @@ interface INotionParams { resource: string; body: JSONData; notionToken?: string; + baseUrl?: string; } const loadPageChunkBody = { @@ -26,8 +28,9 @@ const fetchNotionData = async ({ resource, body, notionToken, + baseUrl = NOTION_API, }: INotionParams): Promise => { - const res = await fetch(`${NOTION_API}/${resource}`, { + const res = await fetch(`${baseUrl}/${resource}`, { method: "POST", headers: { "content-type": "application/json", @@ -77,23 +80,41 @@ const queryCollectionBody = { export const fetchTableData = async ( collectionId: string, collectionViewId: string, - notionToken?: string + notionToken?: string, + blockData?: BlockType ) => { - const table = await fetchNotionData({ - resource: "queryCollection", - body: { - collection: { + const spaceId = blockData?.value?.space_id; + const siteId = blockData?.value?.format?.site_id; + + const apiBaseUrl = siteId + ? `https://${siteId}.notion.site/api/v3` + : NOTION_API; + + + const requestBody: JSONData = { + ...(spaceId ? { + source: { + type: "collection", id: collectionId, + spaceId: spaceId, }, - collectionView: { - id: collectionViewId, - }, - ...queryCollectionBody, + } : {}), + collection: { + id: collectionId, + }, + collectionView: { + id: collectionViewId, + ...(spaceId && { spaceId: spaceId }), }, + ...queryCollectionBody, + } + + return fetchNotionData({ + resource: "queryCollection", + body: requestBody, notionToken, + baseUrl: apiBaseUrl, }); - - return table; }; export const fetchNotionUsers = async ( diff --git a/src/api/types.ts b/src/api/types.ts index bb8b472..7d413ff 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -74,6 +74,10 @@ export interface BaseValueType { last_edited_by_table: string; last_edited_by_id: string; content?: string[]; + space_id?: string; + format?: { + site_id?: string; + }; } export interface CollectionType { diff --git a/src/routes/page.ts b/src/routes/page.ts index d40c844..432abe5 100644 --- a/src/routes/page.ts +++ b/src/routes/page.ts @@ -5,6 +5,7 @@ import { getTableData } from "./table"; import { BlockType, CollectionType, HandlerRequest } from "../api/types"; export async function pageRoute(req: HandlerRequest) { + const pageId = parsePageId(req.params.pageId); const page = await fetchPageById(pageId!, req.notionToken); @@ -75,6 +76,7 @@ export async function pageRoute(req: HandlerRequest) { coll, collView.value.id, req.notionToken, + undefined, true ); diff --git a/src/routes/table.ts b/src/routes/table.ts index 13ef423..3cca0f2 100644 --- a/src/routes/table.ts +++ b/src/routes/table.ts @@ -5,6 +5,7 @@ import { CollectionType, RowType, HandlerRequest, + BlockType, } from "../api/types"; import { createResponse } from "../response"; @@ -12,12 +13,14 @@ export const getTableData = async ( collection: CollectionType, collectionViewId: string, notionToken?: string, + blockData?: BlockType, raw?: boolean ) => { const table = await fetchTableData( collection.value.id, collectionViewId, - notionToken + notionToken, + blockData ); const collectionRows = collection.value.schema; @@ -77,10 +80,14 @@ export async function tableRoute(req: HandlerRequest) { (k) => page.recordMap.collection_view[k] )[0]; + const blockData = page.recordMap.block[pageId!]; + const { rows } = await getTableData( collection, collectionView.value.id, - req.notionToken + req.notionToken, + blockData + ); return createResponse(rows); From 9996f9f74f0fa7a46ef38c03e8dde64f6394f712 Mon Sep 17 00:00:00 2001 From: Trevor Healy Date: Wed, 11 Feb 2026 00:19:48 -0800 Subject: [PATCH 2/2] Handle nested Notion record wrappers in table route --- src/api/utils.ts | 40 ++++++++++++++++- src/routes/table.ts | 104 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 125 insertions(+), 19 deletions(-) diff --git a/src/api/utils.ts b/src/api/utils.ts index 84ba349..f463a6b 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -16,6 +16,42 @@ export const parsePageId = (id: string) => { } }; +// Notion has started returning some recordMap entries wrapped with one or +// more `{ value, role }` envelopes. Normalize to the inner record object. +export const normalizeNotionRecord = (record: any): T => { + let current = record; + + while ( + current && + typeof current === "object" && + current.value && + typeof current.value === "object" && + (Object.prototype.hasOwnProperty.call(current, "role") || + (Object.keys(current).length === 1 && + Object.prototype.hasOwnProperty.call(current, "value"))) + ) { + current = current.value; + } + + return current as T; +}; + +export const getRecordValue = (record: any): T | undefined => { + const normalized = normalizeNotionRecord(record); + if (!normalized) return undefined; + + if ( + typeof normalized === "object" && + normalized.value && + typeof normalized.value === "object" && + Object.prototype.hasOwnProperty.call(normalized, "role") + ) { + return normalized.value as T; + } + + return normalized as T; +}; + export const getNotionValue = ( val: DecorationType[], type: ColumnType, @@ -67,7 +103,9 @@ export const getNotionValue = ( ); url.searchParams.set("table", "block"); - url.searchParams.set("id", row.value.id); + const rowId = + (row as any)?.value?.id || (row as any)?.id || ""; + url.searchParams.set("id", rowId); url.searchParams.set("cache", "v2"); return { name: v[0] as string, url: url.toString(), rawUrl }; diff --git a/src/routes/table.ts b/src/routes/table.ts index 3cca0f2..20386bf 100644 --- a/src/routes/table.ts +++ b/src/routes/table.ts @@ -1,5 +1,5 @@ import { fetchPageById, fetchTableData, fetchNotionUsers } from "../api/notion"; -import { parsePageId, getNotionValue } from "../api/utils"; +import { parsePageId, getNotionValue, getRecordValue } from "../api/utils"; import { RowContentType, CollectionType, @@ -9,6 +9,21 @@ import { } from "../api/types"; import { createResponse } from "../response"; +const resolveBlockValue = (block: any) => { + const normalized = getRecordValue(block); + if (!normalized) return undefined; + if (normalized.properties) return normalized; + if (normalized.value && normalized.value.properties) return normalized.value; + if ( + normalized.value && + normalized.value.value && + normalized.value.value.properties + ) { + return normalized.value.value; + } + return normalized; +}; + export const getTableData = async ( collection: CollectionType, collectionViewId: string, @@ -16,23 +31,52 @@ export const getTableData = async ( blockData?: BlockType, raw?: boolean ) => { + const collectionValue = getRecordValue(collection); + const table = await fetchTableData( - collection.value.id, + collectionValue.id, collectionViewId, notionToken, blockData ); - const collectionRows = collection.value.schema; + const collectionRows = collectionValue.schema; 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 firstBlockId = blockIds[0]; + if (firstBlockId) { + const firstRaw = table.recordMap.block[firstBlockId]; + const firstResolved = resolveBlockValue(firstRaw); + console.log( + `[table-debug] firstId=${firstBlockId} rawKeys=${Object.keys( + firstRaw || {} + ).join(",")} resolvedKeys=${Object.keys(firstResolved || {}).join( + "," + )} hasProps=${Boolean( + firstResolved && firstResolved.properties + )}` + ); + } + + const tableArr: any[] = blockIds + .map((id: string) => resolveBlockValue(table.recordMap.block[id])) + .filter(Boolean); const tableData = tableArr.filter( - (b) => - b.value && b.value.properties && b.value.parent_id === collection.value.id + (b) => b && b.properties && b.parent_id === collectionValue.id + ); + + const parentIds = tableArr + .map((b) => (b ? b.parent_id : undefined)) + .filter(Boolean) + .slice(0, 5) + .join(","); + + console.log( + `[table] ids=${tableArr.length} filtered=${tableData.length} collection=${collectionValue.id} sampleParentIds=${parentIds}` ); type Row = { id: string; [key: string]: RowContentType }; @@ -40,13 +84,15 @@ export const getTableData = async ( const rows: Row[] = []; for (const td of tableData) { - let row: Row = { id: td.value.id }; + let row: Row = { id: td.id }; for (const key of collectionColKeys) { - const val = td.value.properties[key]; + const val = td.properties[key]; if (val) { const schema = collectionRows[key]; - row[schema.name] = raw ? val : getNotionValue(val, schema.type, td); + row[schema.name] = raw + ? val + : getNotionValue(val, schema.type, { value: td } as any); if (schema.type === "person" && row[schema.name]) { const users = await fetchNotionUsers(row[schema.name] as string[]); row[schema.name] = users as any; @@ -70,21 +116,43 @@ export async function tableRoute(req: HandlerRequest) { 401 ); - const collection = Object.keys(page.recordMap.collection).map( + const collection = getRecordValue( + Object.keys(page.recordMap.collection).map( (k) => page.recordMap.collection[k] - )[0]; + )[0] + ); - const collectionView: { + const collectionView = getRecordValue<{ value: { id: CollectionType["value"]["id"] }; - } = Object.keys(page.recordMap.collection_view).map( + }>( + Object.keys(page.recordMap.collection_view).map( (k) => page.recordMap.collection_view[k] - )[0]; + )[0] + ); + + const blockData = getRecordValue(page.recordMap.block[pageId!]) as any; + const collectionId = (collection as any)?.id || (collection as any)?.value?.id; + const collectionViewId = + (collectionView as any)?.id || (collectionView as any)?.value?.id; + + console.log( + `[tableRoute] page=${pageId} collection=${collectionId} view=${collectionViewId}` + ); - const blockData = page.recordMap.block[pageId!]; + if (!collection || !collectionView || !collectionId || !collectionViewId) { + return createResponse( + JSON.stringify({ + error: "Failed to resolve collection or collection view", + pageId, + }), + {}, + 500 + ); + } const { rows } = await getTableData( collection, - collectionView.value.id, + collectionViewId as string, req.notionToken, blockData