2024-04-14
-1
-1
{ "object": "block", "id": "72aec0d6-59c2-48ca-9a55-f8d000fdc56e", "parent": { ... }, "created_time": "2024-03-02T04:41:00.000Z", "last_edited_time": "2024-03-09T09:21:00.000Z", "created_by": { ... }, "last_edited_by": { ... }, "has_children": false, "archived": false, "type": "image", "image": { "caption": [ ... ], "type": "file", "file": { "url": "https://prod-files-secure.s3.us-west-2.amazonaws.com/3175668c-d64e-4b9b-87fc-5fdfe186dc33/b7acf74e-4442-47bf-82ec-6890f97e714b/mountains.avif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45HZZMZUHI%2F20240323%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20240323T013017Z&X-Amz-Expires=3600&X-Amz-Signature=3f0eedc342fc8ad12fc46b1f4eb5829b7f2ba78ba19fc7eb748107af61e1b9ce&X-Amz-SignedHeaders=host&x-id=GetObject", "expiry_time": "2024-03-23T02:30:17.991Z" } } }
"expiry_time": "2024-03-23T02:30:17.991Z"
export default { async scheduled(event, env, ctx) { // this is non-blocking ctx.waitUntil(sync(env)); }, async fetch(request, env, ctx) { // this is blocking return await sync(env); }, };
return new Response('Data fetched and stored successfully.', { status: 200 });
async function getPageContentFromID(env, id) { var requestOptions = { method: "GET", headers: { "Notion-Version": "2022-06-28", // use your secure environment variable Authorization: "Bearer " + env.NOTION_API_KEY, }, }; try { // 100 blocks in a single fetch request is Notion's max. 100 is a lot but if // you want to grab more, you'll need to implement some sort of pagination // algorithm. experiment with moving Notion's cursor and their 'has_more' // field const numBlocksToGrab = 100; const fetchResponseBlocks = await fetch( `https://api.notion.com/v1/blocks/${id}/children?page_size=${numBlocksToGrab}`, requestOptions ); if (!fetchResponseBlocks.ok) { throw new Error(`Couldn't get page blocks: ${fetchResponseBlocks.status}`); } return await fetchResponseBlocks.json(); } catch (error) { console.error('Error fetching page content:', error); // this function will be called in a try-catch to return a Response object // that contains this error information throw error; } }
async function getDatabase(env) { var databaseRequestOptions = { method: "POST", headers: { Authorization: "Bearer " + env.NOTION_API_KEY, "Notion-Version": "2022-06-28", "Content-Type": "application/json", }, }; try { const fetchResponse = await fetch( `https://api.notion.com/v1/databases/${env.NOTION_DATABASE_ID}/query`, databaseRequestOptions ); if (!fetchResponse.ok) { throw new Error(`Couldn't get database: ${fetchResponse.status}`); } const jsonResponse = await fetchResponse.json(); return jsonResponse.results; } catch (error) { console.error('Error fetching published posts:', error); throw error; } }
async function sync(env) { try { // get both published and unpublished data to store AND erase objects. This // is considered 'syncing', and not just 'pushing new data` const db = await getDatabase(env); // or replace with your CMS get-data const published = db.filter(result => { return result.properties.Published.checkbox; }) const unpublished = db.filter(result => { return !result.properties.Published.checkbox; }) const imageIDs = []; // store promises to execute in parallel. efficient for large operations const storeImagePromises = []; const deleteImagePromises = []; for (let pub of published) { // you can replace this with your CMS's way to grab images const pageContent = await getPageContentFromID(env, pub.id); // store images, and record which ones were stored for (let block of pageContent.results) { if (block.type !== "image") continue; const imageID = block.id; imageIDs.push(imageID); // !!! we use the imageID (block's id) for this image's key. this is // important when we want to grab it later. notion's block ID's are // always static, so this is safe storeImagePromises.push(storeImage(env, imageID, block.image.file.url)); } } // remove unused images. 'list' only retrieves the first 1000 items. larger // buckets will need to implement pagination to handle more const r2Objects = await env.SNUGL_NOTION_IMAGES.list(); for (const object of r2Objects.objects) { if (!imageIDs.includes(object.key)) { deleteImagePromises.push(env.SNUGL_NOTION_IMAGES.delete(object.key)); } } await Promise.all(storeImagePromises); await Promise.all(deleteImagePromises); return new Response('Data fetched and stored successfully.', { status: 200, }); } catch (error) { // .. } } async function storeImage(env, id, url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch image. Status: ${response.status}`); } const data = await response.arrayBuffer(); await env.SNUGL_NOTION_IMAGES.put(id, data, { httpMetadata: { contentType: response.headers.get("Content-Type") || "application/octet-stream", }, }); }
export default { async fetch(request, env, ctx) { const url = new URL(request.url); const pathParts = url.pathname.split("/").filter((p) => p); // expecting "/r2/<key_name>" if (pathParts.length !== 2) { return new Response("Invalid URL format", { status: 400 }); } const [storageType, key] = pathParts; switch (storageType) { case "r2": return handleR2Request(key, env); default: return new Response("Invalid storage type.", { status: 400 }); } }, }; async function handleR2Request(key, env) { const object = await env.SNUGL_NOTION_IMAGES.get(key); if (object) { return new Response(object.body, { headers: { "Content-Type": object.httpMetadata.contentType }, status: 200, }); } else { return new Response("Object not found in R2", { status: 404 }); } }
<Image className="..." src={`https://<your.website.api>/r2/${id}`}/>
export default { async scheduled(event, env, ctx) { // 2700 calls per 15 mins is 180 calls per minute. For now I'll have it // sync to notion every 4 minutes. I can edit this in cron jobs. This // is non-blocking. ctx.waitUntil(sync(env)); }, async fetch(request, env, ctx) { // This is blocking return await sync(env); }, }; // Note that this code doesn't handle pagination. async function sync(env) { try { // we get both publish and unpublished data to store/erase objects. this // is considered 'syncing' const db = await getDatabase(env); const published = db.filter(result => { return result.properties.Published.checkbox; }) const unpublished = db.filter(result => { return !result.properties.Published.checkbox; }) const posts = []; const searchData = []; const imageIDs = []; // store promises to execute in parallel. efficient for large operations const storeImagePromises = []; const deleteImagePromises = []; for (let pub of published) { const props = pub.properties; const post = { title: props.Name.title[0].plain_text, slug: props.slug.rich_text[0].plain_text, date: props.Date.date.start, status: props.Status.multi_select[0].name, summary: props.Summary.rich_text[0].plain_text, }; posts.push(post); searchData.push({ slug: post.slug, title: post.title }); const pageContent = await getPageContentFromID(env, pub.id); await env.SNUGL_NOTION_TEXT.put(post.slug, JSON.stringify({ title: post.title, date: post.date, summary: post.summary, content: pageContent })); // store images, and record which ones were stored for (let block of pageContent.results) { if (block.type !== "image") continue; const imageID = block.id; imageIDs.push(imageID); storeImagePromises.push(storeImage(env, imageID, block.image.file.url)); } } posts.sort((a, b) => Date.parse(a.date) - Date.parse(b.date)); // store search data and metadata of posts const search_key = "search_data"; await env.SNUGL_NOTION_TEXT.put(search_key, JSON.stringify(searchData)); const posts_key = "all_posts_details"; await env.SNUGL_NOTION_TEXT.put(posts_key, JSON.stringify(posts)); // remove unpublish articles for (let unpub of unpublished) { const unpubSlug = unpub.properties.slug.rich_text[0].plain_text; await env.SNUGL_NOTION_TEXT.delete(unpubSlug); } // remove unused images const r2Objects = await env.SNUGL_NOTION_IMAGES.list(); // only reteives first 1000 for (const object of r2Objects.objects) { if (!imageIDs.includes(object.key)) { deleteImagePromises.push(env.SNUGL_NOTION_IMAGES.delete(object.key)); } } await Promise.all(storeImagePromises); await Promise.all(deleteImagePromises); return new Response('Data fetched and stored successfully.', { status: 200, }); } catch (error) { try { await storeErrorInKV(env, error); return new Response(error.message, { status: 500, }); } catch (storeError) { console.error('Failed to store error in KV:', storeError); return new Response('Internal Server Error', { status: 500, }); } } } async function storeImage(env, id, url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch image. Status: ${response.status}`); } const data = await response.arrayBuffer(); await env.SNUGL_NOTION_IMAGES.put(id, data, { httpMetadata: { contentType: response.headers.get("Content-Type") || "application/octet-stream", }, }); } async function getDatabase(env) { var databaseRequestOptions = { method: "POST", headers: { Authorization: "Bearer " + env.NOTION_API_KEY, "Notion-Version": "2022-06-28", "Content-Type": "application/json", }, }; try { const fetchResponse = await fetch( `https://api.notion.com/v1/databases/${env.NOTION_DATABASE_ID}/query`, databaseRequestOptions ); if (!fetchResponse.ok) { throw new Error(`Couldn't get database: ${fetchResponse.status}`); } const jsonResponse = await fetchResponse.json(); return jsonResponse.results; } catch (error) { console.error('Error fetching published posts:', error); throw error; } } async function getPageContentFromID(env, id) { var requestOptions = { method: "GET", headers: { "Notion-Version": "2022-06-28", Authorization: "Bearer " + env.NOTION_API_KEY, }, }; try { // 100 is max. I can get more blocks by adjusting the cursor // TODO: if the number of blocks in the page is >100, move // the cursor and grab the next 100 blocks. repeat. I can use // the has_more boolean in the response to check. const numBlocksToGrab = 100; const fetchResponseBlocks = await fetch( `https://api.notion.com/v1/blocks/${id}/children?page_size=${numBlocksToGrab}`, requestOptions ); if (!fetchResponseBlocks.ok) { throw new Error(`Couldn't get page blocks: ${fetchResponseBlocks.status}`); } return await fetchResponseBlocks.json(); } catch (error) { console.error('Error fetching page content:', error); throw error; } } async function storeErrorInKV(env, error) { try { await env.SNUGL_NOTION_TEXT.put('ERROR', JSON.stringify({ time: new Date().toString(), message: error.message, stack: error.stack })); console.error('Error stored: ', error); } catch (storeError) { console.error('Failed to store error in KV:', storeError); } }
export default { async fetch(request, env, ctx) { const url = new URL(request.url); const pathParts = url.pathname.split('/').filter(p => p); // Expecting "/kv/keyname" or "/r2/keyname".' if (pathParts.length !== 2) { return new Response('Invalid URL format', { status: 400 }); } const [storageType, key] = pathParts; switch (storageType) { case 'kv': return handleKVRequest(key, env); case 'r2': return handleR2Request(key, env); default: return new Response('Invalid storage type.', { status: 400 }); } }, }; async function handleKVRequest(key, env) { const data = await env.SNUGL_NOTION_TEXT.get(key); if (data) { return new Response(data, { headers: { 'Content-Type': 'application/json' }, status: 200 }); } else { return new Response('Key not found in KV', { status: 404 }); } } async function handleR2Request(key, env) { const object = await env.SNUGL_NOTION_IMAGES.get(key); if (object) { return new Response(object.body, { headers: { 'Content-Type': object.httpMetadata.contentType }, status: 200 }); } else { return new Response('Object not found in R2', { status: 404 }); } }