When I request collaborators, I want to fetch both clients and collaborators.
Only provide the changed parts
const NOTION_VERSION = ‘2022-06-28’;
const CACHE_SECONDS = 60;
// --- utils ---
const getOrigin = (req) => {
const raw = req.headers.get(‘Origin’);
if (!raw) return null;
try { return new URL(raw).origin; } catch { return null; }
};
const corsHeaders = (origin) => ({
‘Access-Control-Allow-Origin’: origin ?? ‘null’,
‘Access-Control-Allow-Methods’: ‘GET,POST,OPTIONS’,
‘Access-Control-Allow-Headers’: ‘Content-Type’,
‘Vary’: ‘Origin’,
});
const json = (data, { status = 200, origin = null } = {}) =>
new Response(JSON.stringify(data), {
status,
headers: {
…corsHeaders(origin),
‘Content-Type’: ‘application/json’,
‘Cache-Control’: public, max-age=${CACHE_SECONDS},
},
});
function parseAllowedOrigins(env) {
const raw = (env.ALLOWED_ORIGINS || ”)
.split(’,’)
.map(s => s.trim())
.filter(Boolean);
// normalize: lower-case host, strip trailing slash
const norm = raw.map(o => {
u.pathname = ''; u.search = ''; u.hash = '';
return `${u.protocol}//${u.host}`; // e.g. https://app.pagescms.org
}
// --- Notion ---
async function queryNotion({ token, dbId, type }) {
const baseBody = {
page_size: 100,
filter: {
and: [
{ property: ‘Type’, select: { equals: type } },
{ property: ‘Public’, checkbox: { equals: true } },
{ property: ‘Project_Count’, rollup: { number: { greater_than: 0 } } },
],
},
sorts: [{ property: ‘Name’, direction: ‘ascending’ }],
};
let results = [];
let cursor;
do {
const r = await fetch(https://api.notion.com/v1/databases/${dbId}/query, {
method: ‘POST’,
headers: {
Authorization: Bearer ${token},
‘Notion-Version’: NOTION_VERSION,
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(cursor ? { …baseBody, start_cursor: cursor } : baseBody),
});
if (!r.ok) throw new Error(Notion ${r.status}: ${await r.text()});
const data = await r.json();
results = results.concat(data.results || []);
cursor = data.has_more ? data.next_cursor : undefined;
} while (cursor);
return results;
}
const pageTitle = (p) =>
p?.properties?.Name?.title?.[0]?.plain_text ??
p?.properties?.Title?.title?.[0]?.plain_text ??
p?.properties?.Label?.title?.[0]?.plain_text ??
p?.id?.slice(0, 8);
// --- worker ---
export default {
async fetch(request, env) {
const url = new URL(request.url);
const origin = getOrigin(request);
const ALLOWED_ORIGINS = parseAllowedOrigins(env);
const allowed = origin && ALLOWED_ORIGINS.has(origin);
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders(allowed ? origin : null) });
return json({ error: 'Forbidden' }, { status: 403, origin: null });
if (url.pathname === '/api/health') {
return new Response('ok', { status: 200, headers: corsHeaders(origin) });
const token = env.NOTION_TOKEN;
const dbId = env.NOTION_COMMISSIONERS_DB_ID;
return json({ error: 'Missing NOTION_TOKEN or NOTION_COMMISSIONERS_DB_ID' }, { status: 500, origin });
// Make path prefix-agnostic: /api/clients -> /clients
const path = url.pathname.startsWith('/api/notion/') ? url.pathname.slice(11) : url.pathname;
// Map routes to Notion "Type"
'/collaborators': 'Collaborator',
const type = (request.method === 'GET') ? typeMap[path] : undefined;
return json({ error: 'Not found' }, { status: 404, origin });
const pages = await queryNotion({ token, dbId, type });
// .map((p) => ({ id: p.id, name: pageTitle(p) })) // use page.id for stable IDs
// .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true }));
const items = pages .map((p) =>
{ const name = pageTitle(p);
return { id: name, name };
}) .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true }));
return json({ items }, { origin });
return json({ error: 'Upstream error', details: String(err?.message || err) }, { status: 502, origin });
},
};