// Notion Integration for Storyline Database Management
import { Client } from '@notionhq/client';
import { z } from 'zod';
import { NOTION_CONFIG, NOTION_DATABASE_NAMES, validateNotionConfig } from '../config/notion-config.js';
class StorylineNotionSync {
constructor() {
this.notion = null;
this.databaseIds = {
storylines: null,
scenes: null,
characterDevelopment: null,
storyArcs: null
};
}
async getNotionClient() {
if (this.notion) return this.notion;
// Get Notion connection using the same pattern as character pipeline
let connectionSettings;
const hostname = process.env.REPLIT_CONNECTORS_HOSTNAME;
const xReplitToken = process.env.REPL_IDENTITY
? 'repl ' + process.env.REPL_IDENTITY
: process.env.WEB_REPL_RENEWAL
? 'depl ' + process.env.WEB_REPL_RENEWAL
: null;
if (!xReplitToken) {
throw new Error('X_REPLIT_TOKEN not found for repl/depl');
}
connectionSettings = await fetch(
'https://' + hostname + '/api/v2/connection?include_secrets=true&connector_names=notion',
{
headers: {
'Accept': 'application/json',
'X_REPLIT_TOKEN': xReplitToken
}
}
).then(res => res.json()).then(data => data.items?.[0]);
const accessToken = connectionSettings?.settings?.access_token || connectionSettings.settings?.oauth?.credentials?.access_token;
if (!connectionSettings || !accessToken) {
throw new Error('Notion not connected');
}
this.notion = new Client({ auth: accessToken });
return this.notion;
}
// Create Notion databases for storylines if they don't exist
async ensureStorylineDatabases() {
const notion = await this.getNotionClient();
// Validate configuration first
const validation = validateNotionConfig();
if (!validation.isValid) {
console.log('⚠️ Notion configuration incomplete:', validation.issues.join(', '));
return null;
}
try {
// Check if databases already exist by searching
if (NOTION_CONFIG.STORYLINES_DB) {
try {
await notion.databases.retrieve({ database_id: NOTION_CONFIG.STORYLINES_DB });
this.databaseIds.storylines = NOTION_CONFIG.STORYLINES_DB;
console.log('✅ Using existing Storylines database');
} catch (error) {
console.log('⚠️ Configured Storylines database ID invalid, will create new one');
}
}
// Create Storylines database if not exists
if (!this.databaseIds.storylines) {
const parentPageId = NOTION_CONFIG.STORYLINES_PARENT_PAGE || NOTION_CONFIG.DEFAULT_PARENT_PAGE;
const storylinesDb = await notion.databases.create({
parent: { type: "page_id", page_id: parentPageId },
title: [{ type: "text", text: { content: NOTION_DATABASE_NAMES.STORYLINES } }],
properties: {
"Title": { title: {} },
"Description": { rich_text: {} },
"Genre": { select: {
options: [
{ name: "Comedy", color: "yellow" },
{ name: "Drama", color: "red" },
{ name: "Adventure", color: "green" },
{ name: "Educational", color: "blue" },
{ name: "Fantasy", color: "purple" }
]
}},
"Status": { select: {
options: [
{ name: "Draft", color: "gray" },
{ name: "In Production", color: "orange" },
{ name: "Completed", color: "green" },
{ name: "On Hold", color: "red" }
]
}},
"Total Scenes": { number: {} },
"Total Duration": { number: {} },
"Created": { created_time: {} },
"Last Updated": { last_edited_time: {} }
}
});
this.databaseIds.storylines = storylinesDb.id;
console.log(`✅ Created Storylines database: ${storylinesDb.id}`);
}
// Create Scenes database if not exists
if (NOTION_CONFIG.SCENES_DB) {
try {
await notion.databases.retrieve({ database_id: NOTION_CONFIG.SCENES_DB });
this.databaseIds.scenes = NOTION_CONFIG.SCENES_DB;
console.log('✅ Using existing Scenes database');
} catch (error) {
console.log('⚠️ Configured Scenes database ID invalid, will create new one');
}
}
if (!this.databaseIds.scenes) {
const parentPageId = NOTION_CONFIG.SCENES_PARENT_PAGE || NOTION_CONFIG.DEFAULT_PARENT_PAGE;
const scenesDb = await notion.databases.create({
parent: { type: "page_id", page_id: parentPageId },
title: [{ type: "text", text: { content: NOTION_DATABASE_NAMES.SCENES } }],
properties: {
"Scene Title": { title: {} },
"Storyline": { relation: { database_id: this.databaseIds.storylines } },
"Scene Number": { number: {} },
"Description": { rich_text: {} },
"Dialogue": { rich_text: {} },
"Visual Direction": { rich_text: {} },
"Scene Type": { select: {
options: [
{ name: "Dialogue", color: "blue" },
{ name: "Action", color: "red" },
{ name: "Montage", color: "green" },
{ name: "Transition", color: "gray" }
]
}},
"Duration": { number: {} },
"Status": { select: {
options: [
{ name: "Draft", color: "gray" },
{ name: "Ready", color: "yellow" },
{ name: "In Production", color: "orange" },
{ name: "Complete", color: "green" }
]
}},
"Characters": { multi_select: {} }
}
});
this.databaseIds.scenes = scenesDb.id;
console.log(`✅ Created Scenes database: ${scenesDb.id}`);
}
// Create Character Development database if not exists
if (NOTION_CONFIG.CHARACTER_DEVELOPMENT_DB) {
try {
await notion.databases.retrieve({ database_id: NOTION_CONFIG.CHARACTER_DEVELOPMENT_DB });
this.databaseIds.characterDevelopment = NOTION_CONFIG.CHARACTER_DEVELOPMENT_DB;
console.log('✅ Using existing Character Development database');
} catch (error) {
console.log('⚠️ Configured Character Development database ID invalid, will create new one');
}
}
if (!this.databaseIds.characterDevelopment) {
const parentPageId = NOTION_CONFIG.CHARACTER_DEV_PARENT_PAGE || NOTION_CONFIG.DEFAULT_PARENT_PAGE;
const devDb = await notion.databases.create({
parent: { type: "page_id", page_id: parentPageId },
title: [{ type: "text", text: { content: NOTION_DATABASE_NAMES.CHARACTER_DEVELOPMENT } }],
properties: {
"Development Event": { title: {} },
"Character": { relation: { database_id: NOTION_CONFIG.CHARACTERS_MASTER_DB } },
"Storyline": { relation: { database_id: this.databaseIds.storylines } },
"Scene": { relation: { database_id: this.databaseIds.scenes } },
"Development Type": { select: {
options: [
{ name: "Growth", color: "green" },
{ name: "Realization", color: "yellow" },
{ name: "Conflict", color: "red" },
{ name: "Resolution", color: "blue" },
{ name: "Backstory", color: "purple" }
]
}},
"Description": { rich_text: {} },
"Emotional Impact": { number: {} },
"Significance": { select: {
options: [
{ name: "Minor", color: "gray" },
{ name: "Major", color: "orange" },
{ name: "Pivotal", color: "red" }
]
}},
"Created": { created_time: {} }
}
});
this.databaseIds.characterDevelopment = devDb.id;
console.log(`✅ Created Character Development database: ${devDb.id}`);
}
console.log('✅ Notion storyline databases ensured successfully');
return this.databaseIds;
} catch (error) {
console.error('❌ Error creating Notion databases:', error);
throw error;
}
}
// Sync storyline from PostgreSQL to Notion
async syncStorylineToNotion(storylineData) {
const notion = await this.getNotionClient();
try {
const page = await notion.pages.create({
parent: { database_id: this.databaseIds.storylines },
properties: {
"Title": { title: [{ type: "text", text: { content: storylineData.title } }] },
"Description": { rich_text: [{ type: "text", text: { content: storylineData.description || '' } }] },
"Genre": { select: { name: storylineData.genre } },
"Status": { select: { name: storylineData.status || 'Draft' } },
"Total Scenes": { number: storylineData.total_scenes || 0 },
"Total Duration": { number: storylineData.total_duration || 0 }
}
});
return {
success: true,
notion_page_id: page.id,
message: `Storyline "${storylineData.title}" synced to Notion`
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to sync storyline to Notion: ${error.message}`
};
}
}
// Sync scene from PostgreSQL to Notion
async syncSceneToNotion(sceneData, storylineNotionId) {
const notion = await this.getNotionClient();
try {
const page = await notion.pages.create({
parent: { database_id: this.databaseIds.scenes },
properties: {
"Scene Title": { title: [{ type: "text", text: { content: sceneData.title } }] },
"Storyline": { relation: [{ id: storylineNotionId }] },
"Scene Number": { number: sceneData.scene_number },
"Description": { rich_text: [{ type: "text", text: { content: sceneData.description || '' } }] },
"Dialogue": { rich_text: [{ type: "text", text: { content: sceneData.dialogue || '' } }] },
"Visual Direction": { rich_text: [{ type: "text", text: { content: sceneData.visual_direction || '' } }] },
"Scene Type": { select: { name: sceneData.scene_type || 'Dialogue' } },
"Duration": { number: sceneData.duration || 0 },
"Status": { select: { name: sceneData.status || 'Draft' } }
}
});
return {
success: true,
notion_page_id: page.id,
message: `Scene "${sceneData.title}" synced to Notion`
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to sync scene to Notion: ${error.message}`
};
}
}
// Sync character development to Notion
async syncCharacterDevelopmentToNotion(devData, characterNotionId, storylineNotionId, sceneNotionId) {
const notion = await this.getNotionClient();
try {
const page = await notion.pages.create({
parent: { database_id: this.databaseIds.characterDevelopment },
properties: {
"Development Event": { title: [{ type: "text", text: { content: `${devData.development_type}: ${devData.description.substring(0, 100)}...` } }] },
"Character": { relation: [{ id: characterNotionId }] },
"Storyline": { relation: [{ id: storylineNotionId }] },
"Scene": { relation: [{ id: sceneNotionId }] },
"Development Type": { select: { name: devData.development_type } },
"Description": { rich_text: [{ type: "text", text: { content: devData.description } }] },
"Emotional Impact": { number: devData.emotional_impact || 5 },
"Significance": { select: { name: devData.significance || 'Minor' } }
}
});
return {
success: true,
notion_page_id: page.id,
message: `Character development event synced to Notion`
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to sync character development to Notion: ${error.message}`
};
}
}
// Get storyline data from Notion
async getStorylineFromNotion(notionPageId) {
const notion = await this.getNotionClient();
try {
const page = await notion.pages.retrieve({ page_id: notionPageId });
const properties = page.properties;
return {
success: true,
storyline: {
title: properties.Title?.title?.[0]?.text?.content || '',
description: properties.Description?.rich_text?.[0]?.text?.content || '',
genre: properties.Genre?.select?.name || '',
status: properties.Status?.select?.name || 'Draft',
total_scenes: properties['Total Scenes']?.number || 0,
total_duration: properties['Total Duration']?.number || 0,
notion_page_id: notionPageId
}
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to retrieve storyline from Notion: ${error.message}`
};
}
}
}
export { StorylineNotionSync };