Skip to content

Commit ff559eb

Browse files
committed
Enhance Notion sync script with database property validation and improved sync workflow
- Added `ensureDatabaseProperties()` function to validate and create required Notion database properties - Defined `REQUIRED_PROPERTIES` configuration for consistent database schema - Updated script to run property validation before markdown synchronization - Expanded test suite to cover database property validation and bidirectional sync - Improved error handling and logging for database property management
1 parent 24fcd44 commit ff559eb

File tree

2 files changed

+315
-53
lines changed

2 files changed

+315
-53
lines changed

scripts/notion-sync.ts

Lines changed: 194 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ if(!databaseId) {
5858
}
5959
const git = simpleGit();
6060

61+
// Required database properties
62+
const REQUIRED_PROPERTIES = {
63+
title: { type: 'title', name: 'title' },
64+
description: { type: 'rich_text', name: 'description' },
65+
published: { type: 'checkbox', name: 'published' },
66+
date: { type: 'date', name: 'date' },
67+
tags: { type: 'rich_text', name: 'tags' },
68+
editor: { type: 'rich_text', name: 'editor' },
69+
dateCreated: { type: 'date', name: 'dateCreated' }
70+
};
71+
6172
async function syncMarkdownFilesToNotion() {
6273
console.log("Starting syncMarkdownFilesToNotion");
6374
const markdownFiles = await getMarkdownFiles("./");
@@ -235,38 +246,114 @@ async function getNotionPageByTitle(title: string): Promise<any | null> {
235246
}
236247
}
237248

238-
// Update Notion page
239-
async function updateNotionPage(pageId: string, metadata: MarkdownMetadata, content: string) {
240-
try {
241-
const updateData = {
242-
page_id: pageId,
243-
properties: {
244-
description: { rich_text: [{ text: { content: metadata.description } }] },
245-
published: { checkbox: metadata.published },
246-
date: { date: { start: metadata.date } },
247-
tags: { rich_text: [{ text: { content: metadata.tags } }] },
248-
editor: { rich_text: [{ text: { content: metadata.editor } }] },
249-
dateCreated: { date: { start: metadata.dateCreated } },
250-
}
251-
};
252-
await notion.pages.update(updateData);
253-
254-
// Update content in a separate call
255-
await notion.blocks.children.append({
256-
block_id: pageId,
257-
children: [
258-
{
249+
// Convert markdown content to Notion blocks
250+
function markdownToBlocks(markdown: string): any[] {
251+
const blocks: any[] = [];
252+
const lines = markdown.split('\n');
253+
let currentBlock: any = null;
254+
255+
for (let line of lines) {
256+
// Handle headers
257+
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
258+
if (headerMatch) {
259+
const level = headerMatch[1].length;
260+
const text = headerMatch[2];
261+
blocks.push({
262+
object: "block",
263+
type: `heading_${level}`,
264+
[`heading_${level}`]: {
265+
rich_text: [{ type: "text", text: { content: text } }]
266+
}
267+
});
268+
continue;
269+
}
270+
271+
// Handle code blocks
272+
if (line.startsWith('```')) {
273+
if (!currentBlock) {
274+
currentBlock = {
259275
object: "block",
260-
type: "paragraph",
261-
paragraph: {
262-
rich_text: [{ type: "text", text: { content } }]
276+
type: "code",
277+
code: {
278+
language: line.slice(3) || "plain text",
279+
rich_text: [{ type: "text", text: { content: "" } }]
263280
}
281+
};
282+
} else {
283+
blocks.push(currentBlock);
284+
currentBlock = null;
285+
}
286+
continue;
287+
}
288+
289+
// Add content to code block
290+
if (currentBlock?.type === "code") {
291+
currentBlock.code.rich_text[0].text.content += line + "\n";
292+
continue;
293+
}
294+
295+
// Handle bullet points
296+
if (line.match(/^[\-\*]\s/)) {
297+
blocks.push({
298+
object: "block",
299+
type: "bulleted_list_item",
300+
bulleted_list_item: {
301+
rich_text: [{
302+
type: "text",
303+
text: { content: line.slice(2) }
304+
}]
264305
}
265-
]
266-
});
267-
} catch (error) {
268-
console.error("Error in updateNotionPage:", error);
306+
});
307+
continue;
308+
}
309+
310+
// Handle numbered lists
311+
const numberedListMatch = line.match(/^\d+\.\s+(.+)$/);
312+
if (numberedListMatch) {
313+
blocks.push({
314+
object: "block",
315+
type: "numbered_list_item",
316+
numbered_list_item: {
317+
rich_text: [{
318+
type: "text",
319+
text: { content: numberedListMatch[1] }
320+
}]
321+
}
322+
});
323+
continue;
324+
}
325+
326+
// Handle blockquotes
327+
if (line.startsWith('>')) {
328+
blocks.push({
329+
object: "block",
330+
type: "quote",
331+
quote: {
332+
rich_text: [{
333+
type: "text",
334+
text: { content: line.slice(1).trim() }
335+
}]
336+
}
337+
});
338+
continue;
339+
}
340+
341+
// Handle regular paragraphs (including blank lines)
342+
if (line.trim() || blocks.length === 0 || blocks[blocks.length - 1].type !== "paragraph") {
343+
blocks.push({
344+
object: "block",
345+
type: "paragraph",
346+
paragraph: {
347+
rich_text: [{
348+
type: "text",
349+
text: { content: line }
350+
}]
351+
}
352+
});
353+
}
269354
}
355+
356+
return blocks;
270357
}
271358

272359
// Create Notion page
@@ -285,21 +372,45 @@ async function createNotionPage(metadata: MarkdownMetadata, content: string) {
285372
editor: { rich_text: [{ text: { content: metadata.editor } }] },
286373
dateCreated: { date: { start: metadata.dateCreated } },
287374
},
288-
children: [
289-
{
290-
object: "block",
291-
type: "paragraph",
292-
paragraph: {
293-
rich_text: [{ type: "text", text: { content } }]
294-
}
295-
}
296-
]
375+
children: markdownToBlocks(content)
297376
});
298377
} catch (error) {
299378
console.error("Error in createNotionPage:", error);
300379
}
301380
}
302381

382+
// Update Notion page
383+
async function updateNotionPage(pageId: string, metadata: MarkdownMetadata, content: string) {
384+
try {
385+
const updateData = {
386+
page_id: pageId,
387+
properties: {
388+
description: { rich_text: [{ text: { content: metadata.description } }] },
389+
published: { checkbox: metadata.published },
390+
date: { date: { start: metadata.date } },
391+
tags: { rich_text: [{ text: { content: metadata.tags } }] },
392+
editor: { rich_text: [{ text: { content: metadata.editor } }] },
393+
dateCreated: { date: { start: metadata.dateCreated } },
394+
}
395+
};
396+
await notion.pages.update(updateData);
397+
398+
// First delete existing content
399+
const existingBlocks = await notion.blocks.children.list({ block_id: pageId });
400+
for (const block of existingBlocks.results) {
401+
await notion.blocks.delete({ block_id: block.id });
402+
}
403+
404+
// Then add new content as blocks
405+
await notion.blocks.children.append({
406+
block_id: pageId,
407+
children: markdownToBlocks(content)
408+
});
409+
} catch (error) {
410+
console.error("Error in updateNotionPage:", error);
411+
}
412+
}
413+
303414
// Update markdown file from Notion page
304415
async function updateMarkdownFile(filePath: string, notionPage: any) {
305416
// Get properties
@@ -338,19 +449,56 @@ async function updateMarkdownFile(filePath: string, notionPage: any) {
338449
await writeFile(filePath, newFileContent);
339450
}
340451

452+
// Ensure database has required properties
453+
async function ensureDatabaseProperties() {
454+
try {
455+
const database = await notion.databases.retrieve({ database_id: databaseId });
456+
const existingProps = database.properties;
457+
const updates: any = { properties: {} };
458+
let needsUpdate = false;
459+
460+
// Check each required property
461+
for (const [key, config] of Object.entries(REQUIRED_PROPERTIES)) {
462+
if (!existingProps[key]) {
463+
needsUpdate = true;
464+
updates.properties[key] = {
465+
name: config.name,
466+
[config.type]: {}
467+
};
468+
}
469+
}
470+
471+
// Update database if needed
472+
if (needsUpdate) {
473+
console.log('Adding missing properties to database...');
474+
await notion.databases.update({
475+
database_id: databaseId,
476+
...updates
477+
});
478+
console.log('Database properties updated successfully');
479+
}
480+
} catch (error) {
481+
console.error('Error ensuring database properties:', error);
482+
throw error;
483+
}
484+
}
485+
486+
// Only run sync if this is the main module
487+
if (require.main === module) {
488+
ensureDatabaseProperties()
489+
.then(() => syncMarkdownFilesToNotion())
490+
.catch((error) => {
491+
console.error("Global error:", error);
492+
});
493+
}
494+
341495
// Export functions for testing
342496
module.exports = {
343497
getMarkdownFiles,
344498
extractMetadataAndContent,
345499
getNotionPageByTitle,
346500
updateNotionPage,
347501
createNotionPage,
348-
updateMarkdownFile
349-
};
350-
351-
// Only run sync if this is the main module
352-
if (require.main === module) {
353-
syncMarkdownFilesToNotion().catch((error) => {
354-
console.error("Global error:", error);
355-
});
356-
}
502+
updateMarkdownFile,
503+
ensureDatabaseProperties
504+
};

0 commit comments

Comments
 (0)