All checks were successful
thehideout/TheHideout-API/pipeline/head This commit looks good
574 lines
17 KiB
JavaScript
574 lines
17 KiB
JavaScript
import { processNotionIcon } from "./icon";
|
|
import { env } from "cloudflare:workers";
|
|
import { getNavigation } from "./navigation";
|
|
|
|
const NOTION_API_BASE = "https://api.notion.com/v1";
|
|
const NOTION_VERSION = "2025-09-03"; // or your preferred version
|
|
|
|
const iconColorSubstitutions = [
|
|
["#D44C47", "#FF453A"],
|
|
["#55534E", "#FFFFFF"],
|
|
["#448361", "#32D74B"],
|
|
["#337ea9", "#0A84FF"],
|
|
["#9065B0", "#BF5AF2"],
|
|
["#CB912F", "#FFD60A"],
|
|
["#C14C8A", "#FF375F"],
|
|
["#d9730d", "#FF9F0A"],
|
|
];
|
|
|
|
async function notionFetch(endpoint, options = {}) {
|
|
const res = await fetch(`${NOTION_API_BASE}${endpoint}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${env.NOTION_AUTH}`,
|
|
"Notion-Version": NOTION_VERSION,
|
|
"Content-Type": "application/json",
|
|
},
|
|
...options,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(
|
|
`Notion API error: ${res.status} ${res.statusText} - ${text}`
|
|
);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export async function addToNotionDataSource(
|
|
properties,
|
|
dataSourceId,
|
|
icon = null
|
|
) {
|
|
console.log("Adding to Notion data source...");
|
|
console.log("Properties:", properties);
|
|
|
|
const pageParams = {};
|
|
const notionProperties = {};
|
|
|
|
if (icon != null) {
|
|
pageParams["icon"] = {
|
|
type: "external",
|
|
external: {
|
|
url: icon,
|
|
},
|
|
};
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(properties)) {
|
|
if (key === "Email" && value != "") {
|
|
notionProperties[key] = { email: value };
|
|
} else if (key === "Name" && value != "") {
|
|
notionProperties[key] = { title: [{ text: { content: value } }] };
|
|
} else if (key === "Features") {
|
|
notionProperties[key] = {
|
|
multi_select: value.map((feature) => ({
|
|
name: feature,
|
|
})),
|
|
};
|
|
} else if (key === "Message" && value != "") {
|
|
pageParams["children"] = [
|
|
{
|
|
object: "block",
|
|
type: "paragraph",
|
|
paragraph: {
|
|
rich_text: [
|
|
{
|
|
type: "text",
|
|
text: {
|
|
content: value,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
} else if (typeof value === "string" && value != "") {
|
|
notionProperties[key] = {
|
|
rich_text: [{ text: { content: value } }],
|
|
};
|
|
} else if (typeof value === "number" && value != undefined) {
|
|
notionProperties[key] = { number: value };
|
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
notionProperties[key] = value;
|
|
}
|
|
}
|
|
try {
|
|
const response = await notionFetch("/pages", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
parent: { data_source_id: dataSourceId },
|
|
...pageParams,
|
|
properties: notionProperties,
|
|
}),
|
|
});
|
|
|
|
console.log("Added to Notion DB!");
|
|
return response;
|
|
} catch (error) {
|
|
console.error("Failed to add to Notion:", error);
|
|
throw new Error("Failed to add to Notion");
|
|
}
|
|
}
|
|
|
|
export async function queryNotionDataSource(dataSourceId, queryParams = {}) {
|
|
console.log(`Fetching data source: ${dataSourceId} from Notion API...`);
|
|
|
|
try {
|
|
const body = JSON.stringify(queryParams);
|
|
|
|
const result = await notionFetch(`/data_sources/${dataSourceId}/query`, {
|
|
method: "POST",
|
|
body,
|
|
});
|
|
|
|
console.log("Fetched data source:", dataSourceId);
|
|
return result.results;
|
|
} catch (error) {
|
|
console.error(`Error fetching Notion data source ${dataSourceId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getNotionDatabase(databaseId) {
|
|
console.log(`Fetching database: ${databaseId} from Notion API...`);
|
|
|
|
try {
|
|
const result = await notionFetch(`/databases/${databaseId}`, {
|
|
method: "GET",
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`Error fetching Notion database ${databaseId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getNotionDataSource(dataSourceId) {
|
|
console.log(`Fetching data source: ${dataSourceId} from Notion API...`);
|
|
|
|
try {
|
|
const result = await notionFetch(`/data_sources/${dataSourceId}`, {
|
|
method: "GET",
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`Error fetching Notion data source ${dataSourceId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getNotionPage(pageId) {
|
|
console.log(`Fetching page: ${pageId} from Notion API...`);
|
|
try {
|
|
const result = await notionFetch(`/pages/${pageId}`, {
|
|
method: "GET",
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`Error fetching Notion page ${pageId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function updateNotionPage(
|
|
pageId,
|
|
properties,
|
|
archive = false,
|
|
trash = false
|
|
) {
|
|
console.log(`Updating page: ${pageId} in Notion API...`);
|
|
const notionProperties = {};
|
|
|
|
for (const [key, value] of Object.entries(properties)) {
|
|
if (key === "Email" && value != "") {
|
|
notionProperties[key] = { email: value };
|
|
} else if (key === "Name") {
|
|
notionProperties[key] = { title: [{ text: { content: value } }] };
|
|
} else if (key === "Features") {
|
|
notionProperties[key] = {
|
|
multi_select: value.map((feature) => ({
|
|
name: feature,
|
|
})),
|
|
};
|
|
} else if (typeof value === "string" && value != "") {
|
|
notionProperties[key] = {
|
|
rich_text: [{ text: { content: value } }],
|
|
};
|
|
} else if (typeof value === "number" && value != undefined) {
|
|
notionProperties[key] = { number: value };
|
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
notionProperties[key] = value;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await notionFetch(`/pages/${pageId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({
|
|
properties: notionProperties,
|
|
archived: archive,
|
|
in_trash: trash,
|
|
}),
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`Error updating Notion page ${pageId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getNotionBlocks(pageId, queryParams = {}) {
|
|
console.log(`Fetching blocks for page: ${pageId} from Notion API...`);
|
|
|
|
try {
|
|
const result = await notionFetch(
|
|
`/blocks/${pageId}/children?${new URLSearchParams(queryParams)}`
|
|
);
|
|
return result.results;
|
|
} catch (error) {
|
|
console.error(`Error fetching Notion page ${pageId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getNotionSVGIcon(url) {
|
|
try {
|
|
console.log(`Fetching SVG icon from: ${url}`);
|
|
const response = await fetch(url);
|
|
if (!response.ok)
|
|
throw new Error(`Failed to fetch SVG: ${response.statusText}`);
|
|
|
|
let svgText = await response.text();
|
|
iconColorSubstitutions.forEach(([searchColor, replaceColor]) => {
|
|
svgText = svgText.replaceAll(searchColor, replaceColor);
|
|
});
|
|
|
|
return svgText;
|
|
} catch (error) {
|
|
console.error(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Transform Notion blocks to content format
|
|
export async function transformContent(env, pageId) {
|
|
try {
|
|
console.log(`Transforming content for page: ${pageId}`);
|
|
|
|
const navigationItems = await getNavigation(env);
|
|
|
|
// Fetch blocks from Notion
|
|
const blocks = await getNotionBlocks(pageId);
|
|
|
|
const content = [];
|
|
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
const block = blocks[i];
|
|
const blockType = block.type;
|
|
|
|
// Group consecutive list items
|
|
if (
|
|
blockType === "bulleted_list_item" ||
|
|
blockType === "numbered_list_item"
|
|
) {
|
|
const isNumbered = blockType === "numbered_list_item";
|
|
const children = [];
|
|
// Collect consecutive list items of the same type
|
|
while (
|
|
i < blocks.length &&
|
|
blocks[i].type ===
|
|
(isNumbered ? "numbered_list_item" : "bulleted_list_item")
|
|
) {
|
|
children.push({
|
|
type: "listItem",
|
|
text: await extractRichText(
|
|
isNumbered
|
|
? blocks[i].numbered_list_item.rich_text
|
|
: blocks[i].bulleted_list_item.rich_text,
|
|
navigationItems
|
|
),
|
|
});
|
|
i++;
|
|
}
|
|
// Step back one index because the for loop will increment it
|
|
i--;
|
|
content.push({
|
|
type: "list",
|
|
ordered: isNumbered,
|
|
children,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
switch (blockType) {
|
|
case "heading_1":
|
|
content.push({
|
|
type: "title1",
|
|
text: await extractRichText(
|
|
block.heading_1.rich_text,
|
|
navigationItems
|
|
),
|
|
});
|
|
break;
|
|
|
|
case "heading_2":
|
|
content.push({
|
|
type: "title2",
|
|
text: await extractRichText(
|
|
block.heading_2.rich_text,
|
|
navigationItems
|
|
),
|
|
});
|
|
break;
|
|
|
|
case "heading_3":
|
|
content.push({
|
|
type: "title3",
|
|
text: await extractRichText(
|
|
block.heading_3.rich_text,
|
|
navigationItems
|
|
),
|
|
});
|
|
break;
|
|
|
|
case "paragraph":
|
|
const paragraphText = await extractRichText(
|
|
block.paragraph.rich_text,
|
|
navigationItems
|
|
);
|
|
if (paragraphText.trim()) {
|
|
content.push({
|
|
type: "paragraph",
|
|
text: paragraphText,
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "divider":
|
|
content.push({
|
|
type: "divider",
|
|
});
|
|
break;
|
|
|
|
case "image":
|
|
content.push({
|
|
type: "image",
|
|
url: block.image.external?.url || block.image.file?.url,
|
|
caption: block.image.caption
|
|
? await extractRichText(block.image.caption, navigationItems)
|
|
: null,
|
|
});
|
|
break;
|
|
|
|
case "quote":
|
|
content.push({
|
|
type: "quote",
|
|
text: await extractRichText(block.quote.rich_text, navigationItems),
|
|
});
|
|
break;
|
|
|
|
case "code":
|
|
content.push({
|
|
type: "code",
|
|
text: await extractRichText(block.code.rich_text, navigationItems),
|
|
language: block.code.language,
|
|
});
|
|
break;
|
|
case "bookmark":
|
|
content.push({
|
|
type: "button",
|
|
text: await extractRichText(
|
|
block.bookmark.caption,
|
|
navigationItems
|
|
),
|
|
url: block.bookmark.url,
|
|
});
|
|
break;
|
|
|
|
case "callout":
|
|
content.push({
|
|
type: "callout",
|
|
text: await extractRichText(
|
|
block.callout.rich_text,
|
|
navigationItems
|
|
),
|
|
icon: block.callout.icon?.emoji || null,
|
|
});
|
|
break;
|
|
case "link_to_page":
|
|
const linkedNavigationItem = getItemByNotionId(
|
|
navigationItems,
|
|
block.link_to_page.page_id
|
|
);
|
|
content.push({
|
|
type: "button",
|
|
url: `/${linkedNavigationItem.slug}`,
|
|
text: linkedNavigationItem.name,
|
|
icon: linkedNavigationItem.icon,
|
|
});
|
|
break;
|
|
default:
|
|
// For unsupported block types, try to extract text if available
|
|
console.log(`Unsupported block type: ${blockType}`);
|
|
console.log(JSON.stringify(block));
|
|
break;
|
|
}
|
|
}
|
|
|
|
return content;
|
|
} catch (error) {
|
|
console.error(`Error transforming page content for ${pageId}:`, error);
|
|
// Return fallback content if transformation fails
|
|
return [
|
|
{
|
|
type: "paragraph",
|
|
text: "Content could not be loaded at this time.",
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
function processLink(text, href, navigationItems) {
|
|
const icon = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
<g>
|
|
<path d="M56.855,1.184L56.816,1.184L9.356,1.184C6.044,1.184 3.356,3.872 3.356,7.184C3.356,10.495 6.044,13.184 9.356,13.184L42.331,13.184L2.941,52.574C0.599,54.915 0.599,58.718 2.941,61.059C5.282,63.401 9.085,63.401 11.426,61.059L50.816,21.669L50.816,54.644C50.816,57.956 53.505,60.644 56.816,60.644C60.128,60.644 62.816,57.956 62.816,54.644L62.816,7.184C62.816,6.374 62.656,5.601 62.364,4.896L62.357,4.879C62.065,4.174 61.632,3.514 61.059,2.941C60.486,2.368 59.826,1.935 59.121,1.643L59.104,1.636C58.827,1.521 58.539,1.427 58.243,1.354L58.204,1.345L58.166,1.336L58.128,1.328L58.09,1.319L58.052,1.311L58.014,1.303L57.988,1.298L57.952,1.291L57.915,1.284L57.878,1.277L57.841,1.271L57.804,1.265L57.767,1.259L57.73,1.253L57.692,1.247L57.655,1.242L57.618,1.237L57.58,1.232L57.543,1.227L57.505,1.223L57.467,1.218L57.43,1.215L57.392,1.211L57.354,1.207L57.316,1.204L57.278,1.201L57.24,1.198L57.202,1.196L57.163,1.193L57.125,1.191L57.087,1.19L57.048,1.188L57.01,1.187L56.971,1.185L56.933,1.185L56.894,1.184L56.855,1.184Z"/>
|
|
</g>
|
|
</svg>
|
|
`;
|
|
|
|
var pageIdNoDashes = null;
|
|
if (href.startsWith("/")) {
|
|
pageIdNoDashes = href.split("/").pop().replaceAll("#", "");
|
|
}
|
|
if (href.includes("notion.so/")) {
|
|
if (href.includes("-")) {
|
|
pageIdNoDashes = href.split("-").pop().replaceAll("#", "");
|
|
} else {
|
|
pageIdNoDashes = href.split("/").pop().replaceAll("#", "");
|
|
}
|
|
}
|
|
|
|
const textSpan = `<span class="th-link-text">${text}</span>`;
|
|
const iconSpan = `<span class="th-link-icon">${icon}</span>`;
|
|
|
|
if (pageIdNoDashes != null) {
|
|
const linkedNavigationItem = getItemByNotionId(
|
|
navigationItems,
|
|
pageIdNoDashes
|
|
);
|
|
return `<a href="/${linkedNavigationItem.slug}" class="th-link">${textSpan}</a>`;
|
|
} else {
|
|
return `<a href="${href}" class="th-link">${textSpan}${iconSpan}</a>`;
|
|
}
|
|
}
|
|
|
|
// Helper function to extract rich text content
|
|
export async function extractRichText(richTextArray, navigationItems) {
|
|
if (!richTextArray || !Array.isArray(richTextArray)) {
|
|
return "";
|
|
}
|
|
|
|
const textPieces = await Promise.all(
|
|
richTextArray.map(async (textObj) => {
|
|
// Handle custom emoji mentions
|
|
if (
|
|
textObj.type === "mention" &&
|
|
textObj.mention?.type === "custom_emoji" &&
|
|
textObj.mention.custom_emoji?.url
|
|
) {
|
|
const emoji = await processNotionIcon(
|
|
textObj.mention.custom_emoji,
|
|
"th-inline-emoji"
|
|
);
|
|
return emoji;
|
|
}
|
|
|
|
let text = textObj.plain_text || "";
|
|
|
|
// Handle formatting
|
|
if (textObj.annotations) {
|
|
const annotations = textObj.annotations;
|
|
|
|
if (annotations.bold) {
|
|
text = `<strong>${text}</strong>`;
|
|
}
|
|
if (annotations.italic) {
|
|
text = `<i>${text}</i>`;
|
|
}
|
|
if (annotations.underline) {
|
|
text = `<u>${text}</u>`;
|
|
}
|
|
if (annotations.strikethrough) {
|
|
text = `<s>${text}</s>`;
|
|
}
|
|
if (annotations.code) {
|
|
text = `<code>${text}</code>`;
|
|
}
|
|
}
|
|
|
|
// Handle links
|
|
if (textObj.href) {
|
|
text = processLink(text, textObj.href, navigationItems);
|
|
}
|
|
|
|
return text;
|
|
})
|
|
);
|
|
|
|
return textPieces.join("");
|
|
}
|
|
|
|
// Utility: safely extract plain text from a Notion title or rich_text field
|
|
export function getPlainText(field) {
|
|
if (!field) return "";
|
|
if (field.type === "title" || field.type === "rich_text") {
|
|
return field[field.type].map((t) => t.plain_text).join("") || "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Utility: convert a string to camelCase
|
|
export function toCamelCase(str = "") {
|
|
return str
|
|
.replace(/[^a-zA-Z0-9 ]/g, " ") // remove non-alphanumeric except spaces
|
|
.trim()
|
|
.split(/\s+/) // split on spaces
|
|
.map((word, index) => {
|
|
if (index === 0) return word.toLowerCase();
|
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
// Moved from properties.js: buildNewCache, now buildListCache
|
|
export function buildListCache(
|
|
cachedList,
|
|
{ added = [], updated = [], deleted = [] }
|
|
) {
|
|
// Remove deleted and items that will be updated
|
|
const filtered = cachedList.filter(
|
|
(p) =>
|
|
!deleted.some((d) => d.notionId === p.notionId) &&
|
|
!updated.some((u) => u.notionId === p.notionId)
|
|
);
|
|
|
|
return [...filtered, ...updated, ...added];
|
|
}
|
|
|
|
export function getItemByNotionId(items, notionId) {
|
|
const itemWithDashes = items.find((item) => item.notionId === notionId);
|
|
if (itemWithDashes) {
|
|
return itemWithDashes;
|
|
}
|
|
const itemWithoutDashes = items.find(
|
|
(item) => item.notionId.replaceAll("-", "") === notionId.replaceAll("-", "")
|
|
);
|
|
if (itemWithoutDashes) {
|
|
return itemWithoutDashes;
|
|
}
|
|
return null;
|
|
}
|