Files

259 lines
6.8 KiB
JavaScript

/**
* Shared utilities for Notion API scripts
* Common functions for HTTP requests, error handling, and data extraction
*/
const https = require('https');
const NOTION_API_KEY = process.env.NOTION_API_KEY;
const NOTION_VERSION = '2025-09-03';
/**
* Make a Notion API request with proper error handling
*/
function notionRequest(path, method, data = null) {
return new Promise((resolve, reject) => {
const requestData = data ? JSON.stringify(data) : null;
const options = {
hostname: 'api.notion.com',
port: 443,
path: path,
method: method,
headers: {
'Authorization': `Bearer ${NOTION_API_KEY}`,
'Notion-Version': NOTION_VERSION,
'Content-Type': 'application/json'
}
};
if (requestData) {
options.headers['Content-Length'] = Buffer.byteLength(requestData);
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(body));
} else {
reject(createDetailedError(res.statusCode, body));
}
});
});
req.on('error', reject);
if (requestData) {
req.write(requestData);
}
req.end();
});
}
/**
* Create detailed error message based on status code and response
*/
function createDetailedError(statusCode, body) {
let error;
try {
error = JSON.parse(body);
} catch (e) {
return new Error(`API error (${statusCode}): ${body}`);
}
const errorCode = error.code;
const errorMessage = error.message;
switch (statusCode) {
case 400:
if (errorCode === 'validation_error') {
return new Error(`Validation error: ${errorMessage}. Check your input data.`);
}
return new Error(`Bad request: ${errorMessage}`);
case 401:
return new Error('Authentication failed. Check your NOTION_API_KEY environment variable.');
case 404:
if (errorCode === 'object_not_found') {
return new Error('Page/database not found. Make sure it is shared with your integration.');
}
return new Error(`Not found: ${errorMessage}`);
case 429:
return new Error('Rate limit exceeded. Wait a moment and try again.');
case 500:
case 503:
return new Error(`Notion server error (${statusCode}). Try again later.`);
default:
return new Error(`API error (${statusCode}): ${errorMessage || body}`);
}
}
/**
* Extract title from a page or database object
*/
function extractTitle(item) {
if (item.object === 'page') {
const titleProp = Object.values(item.properties || {}).find(p => p.type === 'title');
if (titleProp && titleProp.title && titleProp.title.length > 0) {
return titleProp.title[0].plain_text;
}
} else if (item.object === 'database' || item.object === 'data_source') {
if (item.title && item.title.length > 0) {
return item.title[0].plain_text;
}
}
return '(Untitled)';
}
/**
* Extract value from a property based on its type
*/
function extractPropertyValue(property) {
switch (property.type) {
case 'title':
return property.title.map(t => t.plain_text).join('');
case 'rich_text':
return property.rich_text.map(t => t.plain_text).join('');
case 'number':
return property.number;
case 'select':
return property.select?.name || null;
case 'multi_select':
return property.multi_select.map(s => s.name);
case 'date':
return property.date ? {
start: property.date.start,
end: property.date.end
} : null;
case 'checkbox':
return property.checkbox;
case 'url':
return property.url;
case 'email':
return property.email;
case 'phone_number':
return property.phone_number;
case 'relation':
return property.relation.map(r => r.id);
case 'created_time':
return property.created_time;
case 'last_edited_time':
return property.last_edited_time;
default:
return property[property.type];
}
}
/**
* Parse rich text with basic markdown formatting
*/
function parseRichText(text) {
const richText = [];
const maxLength = 2000;
// Split text into chunks if needed
if (text.length <= maxLength) {
richText.push({
type: 'text',
text: { content: text }
});
} else {
// Split into chunks
for (let i = 0; i < text.length; i += maxLength) {
richText.push({
type: 'text',
text: { content: text.substring(i, i + maxLength) }
});
}
}
return richText;
}
/**
* Check if NOTION_API_KEY is set
*/
function checkApiKey() {
if (!NOTION_API_KEY) {
console.error('Error: NOTION_API_KEY environment variable not set');
console.error('');
console.error('Setup:');
console.error(' 1. Create a Notion integration at https://www.notion.so/my-integrations');
console.error(' 2. Store the API key in macOS Keychain');
console.error(' 3. Add to environment loader (e.g., ~/.openclaw/bin/openclaw-env.sh):');
console.error(' export NOTION_API_KEY="$(security find-generic-password -a "$USER" -s "openclaw.notion_api_key" -w)"');
console.error(' 4. Restart gateway: openclaw gateway restart');
process.exit(1);
}
}
/**
* Format property value for database operations
*/
function formatPropertyValue(propertyType, value) {
switch (propertyType) {
case 'select':
return { select: { name: value } };
case 'multi_select':
const tags = Array.isArray(value) ? value : value.split(',').map(t => t.trim());
return { multi_select: tags.map(name => ({ name })) };
case 'checkbox':
const boolValue = typeof value === 'boolean' ? value :
(value.toLowerCase() === 'true' || value === '1');
return { checkbox: boolValue };
case 'number':
return { number: typeof value === 'number' ? value : parseFloat(value) };
case 'url':
return { url: value };
case 'email':
return { email: value };
case 'date':
if (typeof value === 'string') {
const dates = value.split(',').map(d => d.trim());
return {
date: {
start: dates[0],
end: dates[1] || null
}
};
}
return { date: value };
case 'rich_text':
return {
rich_text: [{ type: 'text', text: { content: value } }]
};
case 'title':
return {
title: [{ type: 'text', text: { content: value } }]
};
default:
throw new Error(`Unsupported property type: ${propertyType}`);
}
}
module.exports = {
notionRequest,
createDetailedError,
extractTitle,
extractPropertyValue,
parseRichText,
checkApiKey,
formatPropertyValue,
NOTION_API_KEY,
NOTION_VERSION
};