sveltekit-notion-blog
Version:
A plug and play library for setting up blogs in subdirectory or main directory in Sveltekit projects using Notion as CMS.
296 lines (295 loc) • 9.72 kB
JavaScript
import { isFullBlock, isFullPage, isPageObjectResponse } from "../types/helpers";
import { ok, err, Result } from 'neverthrow';
const BASE_URL = "https://api.notion.com/v1";
const sendRequest = async (url, blogClient, method, body) => {
const headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${blogClient.config.notionToken}`,
"Notion-Version": "2022-06-28"
};
const response = await fetch(url, {
method,
headers,
body: JSON.stringify(body)
});
//get response type
const contentType = response.headers.get("Content-Type");
let data;
if (contentType) {
if (contentType?.indexOf("application/json") > -1) {
data = await response.json();
}
else {
data = await response.text();
}
}
if (!response.ok) {
return {
error: data,
data: null
};
}
return {
error: null,
data
};
};
const api = {
get: async (url, blogClient) => {
return sendRequest(url, blogClient, "GET");
},
post: async (url, blogClient, body) => {
return sendRequest(url, blogClient, "POST", body);
}
};
export const getDatabaseById = async (blogClient) => {
try {
const url = `${BASE_URL}/databases/${blogClient.config.databaseId}/query`;
const notion = blogClient.config.notionToken;
if (!notion) {
return err({ code: 400, message: "Invalid or missing notion secret" });
}
const database = await api.post(url, blogClient, {
"filter": {
"property": "Published",
"checkbox": {
"equals": true
}
}
});
const results = database.data?.results;
if (isPageObjectResponse(results) && results?.length > 0) {
const result = results;
return ok(result);
}
else {
return ok([]);
}
}
catch (error) {
return handleNotionError(error);
}
};
export const getBlogSlugs = async (blogClient) => {
try {
const url = `${BASE_URL}/databases/${blogClient.config.databaseId}/query`;
const notion = blogClient.config.notionToken;
if (!notion) {
return err({ code: 400, message: "Invalid or missing notion secret" });
}
const database = await api.post(url, blogClient, {
filter: {
and: [
{
property: "Slug",
rich_text: {
is_not_empty: true
}
},
{
property: "Published",
checkbox: {
"equals": true
},
}
]
}
});
if (database.error) {
return err({ code: 500, message: database.error });
}
const results = database.data?.results;
if (isPageObjectResponse(results) && results?.length > 0) {
const result = results;
//@ts-ignore
const slugs = result.map((page) => page.properties["Slug"].rich_text[0].plain_text);
return ok(slugs);
}
return ok([]);
}
catch (error) {
return handleNotionError(error);
}
};
export const getPageBySlug = async (blogClient, slug) => {
try {
const notion = blogClient.config.notionToken;
if (!notion) {
return err({ code: 400, message: "Invalid or missing notion secret" });
}
const url = `${BASE_URL}/databases/${blogClient.config.databaseId}/query`;
const res = await api.post(url, blogClient, {
"filter": {
"property": "Slug",
"rich_text": {
"equals": slug
}
}
});
let pages = [];
if (res.data) {
for (const page of res.data.results) {
if (!isFullPage(page)) {
continue;
}
pages.push(page);
}
return ok(pages);
}
else {
return err({ code: 500, message: "Some error ocurred" });
}
}
catch (error) {
return handleNotionError(error);
}
};
export const getBlocks = async (blogClient, blockId) => {
if (!blogClient.config.notionToken) {
return err({ code: 400, message: "Invalid or missing notion secret" });
}
let blocks = [];
const results = await getAllBlocksRecursively(blogClient, blockId, blocks, undefined);
if (results.isErr()) {
return err({ code: 500, message: results.error.message });
}
if (results.value.length > 0) {
return ok(results.value);
}
else if (results && results.value.length == 0) {
return ok([]);
}
else {
return err({ code: 500, message: "Unknown Error" });
}
};
const getAllBlocksRecursively = async (blogClient, blockId, blocks, nextCursor) => {
try {
let url = `${BASE_URL}/blocks/${blockId}/children?page_size=100`;
if (nextCursor) {
url += `&start_cursor=${nextCursor}`;
}
const res = await api.get(url, blogClient);
if (res.error) {
return err({ code: 500, message: res.error });
}
if (res.data) {
const { results, has_more, next_cursor } = res.data;
for (const block of results) {
if (!isFullBlock(block))
continue;
blocks.push(block);
}
if (has_more && next_cursor) {
await getAllBlocksRecursively(blogClient, blockId, blocks, next_cursor);
}
}
return ok(blocks);
}
catch (error) {
return handleNotionError(error);
}
};
const handleNotionError = (error) => {
switch (error.code) {
case "notionhq_client_request_timeout":
return err({ code: 400, message: "Request could not be completed" });
case "object_not_found":
return err({ code: 500, message: "Database of page not found" });
case "unauthorized":
return err({ code: 401, message: "User not authorized" });
default:
return err({ code: 400, message: error ? Object.keys(error)?.length > 0 ? "" : "" : "Some error ocurred" });
}
};
export const getNotionUser = async (blogClient, userId) => {
try {
if (!blogClient.config.notionToken) {
return err({ code: 400, message: "Invalid or missing notion secret" });
}
const url = `${BASE_URL}/users/${userId}`;
const response = await api.get(url, blogClient);
if (response.error) {
return err({ code: 500, message: response.error });
}
if (response.data) {
return ok(response.data);
}
return err({ code: 500, message: "Some error ocurred" });
}
catch (error) {
return handleNotionError(error);
}
};
export const getFAQs = async (blogClent, id) => {
const response = await getBlocks(blogClent, id);
if (response.isOk()) {
const faqs = response.value.map((row) => {
if (row.type == "table_row") {
if (row.table_row.cells?.[0]?.[0]?.plain_text == "Question") {
return {
question: null,
answer: null
};
}
else {
return {
question: row.table_row.cells?.[0]?.[0]?.plain_text,
answer: row.table_row.cells?.[1]?.map((r) => r.plain_text)?.join("")
};
}
}
else {
return {
question: null,
answer: null
};
}
});
return ok(faqs);
}
else if (response.isErr()) {
return err(response.error);
}
else {
return err({
code: 500, message: "Unknown Error"
});
}
};
export const groupNumberedListItems = (blocks) => {
const result = [];
let currentGroup = [];
for (let index = 0; index < blocks.length; index++) {
const block = blocks[index];
const prevBlock = blocks[index - 1];
const nextBlock = blocks[index + 1];
if (block.type === 'numbered_list_item') {
if ((!prevBlock || prevBlock?.type !== 'numbered_list_item') && nextBlock?.type === 'numbered_list_item') {
// first item in the doc
currentGroup.push(block);
}
else if (prevBlock.type === 'numbered_list_item' && nextBlock?.type === 'numbered_list_item') {
// next && prev block are numbered list items
currentGroup.push(block);
}
else if (prevBlock?.type === 'numbered_list_item' && nextBlock?.type !== 'numbered_list_item') {
currentGroup.push(block);
// last item in the list
result.push({
type: 'grouped_numbered_list',
items: currentGroup
});
currentGroup = [];
}
else {
// number_list_item but not preceded or succeeded by number_list_item
result.push(block);
}
}
else {
result.push(block);
}
}
return result;
};