UNPKG

roam-research-mcp

Version:
143 lines (142 loc) • 5.29 kB
import { BatchOperations } from './batch.js'; /** * Validates table input before building actions. */ export function validateTableInput(input) { const errors = []; if (!input.parent_uid) { errors.push({ field: 'parent_uid', message: 'parent_uid is required' }); } if (!input.headers || !Array.isArray(input.headers)) { errors.push({ field: 'headers', message: 'headers must be an array' }); } else if (input.headers.length === 0) { errors.push({ field: 'headers', message: 'At least one header is required' }); } if (!input.rows || !Array.isArray(input.rows)) { errors.push({ field: 'rows', message: 'rows must be an array' }); } else { const expectedCells = (input.headers?.length || 1) - 1; for (let i = 0; i < input.rows.length; i++) { const row = input.rows[i]; if (!row.label && row.label !== '') { errors.push({ field: `rows[${i}].label`, message: 'Row label is required (use empty string for blank)' }); } if (!Array.isArray(row.cells)) { errors.push({ field: `rows[${i}].cells`, message: 'Row cells must be an array' }); } else if (row.cells.length !== expectedCells) { errors.push({ field: `rows[${i}].cells`, message: `Expected ${expectedCells} cells, got ${row.cells.length}` }); } } } return { valid: errors.length === 0, errors }; } /** * Builds batch actions for creating a Roam table structure. * * Roam tables have a specific nested structure: * - The table container block contains {{[[table]]}} * - Header row is nested deeply (each column nested under the previous) * - Data rows follow the same nesting pattern */ export function buildTableActions(input) { const actions = []; // Create table container actions.push({ action: 'create-block', uid: '{{uid:table}}', string: '{{[[table]]}}', location: { 'parent-uid': input.parent_uid, order: input.order ?? 'last' } }); // Create header row with nested structure // In Roam tables, each column is nested under the previous column let headerParent = '{{uid:table}}'; for (let i = 0; i < input.headers.length; i++) { const uid = `{{uid:header_${i}}}`; const headerText = input.headers[i] || ' '; // Convert empty to space actions.push({ action: 'create-block', uid, string: headerText, location: { 'parent-uid': headerParent, order: 0 } }); headerParent = uid; } // Create data rows // Each row starts as a child of the table, with cells nested under each other for (let rowIdx = 0; rowIdx < input.rows.length; rowIdx++) { const row = input.rows[rowIdx]; // Row label (first column) - child of table at position rowIdx + 1 (after header) const labelUid = `{{uid:row_${rowIdx}_label}}`; const labelText = row.label || ' '; // Convert empty to space actions.push({ action: 'create-block', uid: labelUid, string: labelText, location: { 'parent-uid': '{{uid:table}}', order: rowIdx + 1 } }); // Row cells - each cell is nested under the previous let cellParent = labelUid; for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) { const cellUid = `{{uid:row_${rowIdx}_cell_${cellIdx}}}`; const cellText = row.cells[cellIdx] || ' '; // Convert empty to space actions.push({ action: 'create-block', uid: cellUid, string: cellText, location: { 'parent-uid': cellParent, order: 0 } }); cellParent = cellUid; } } return actions; } export class TableOperations { constructor(graph) { this.batchOps = new BatchOperations(graph); } /** * Creates a table in Roam with the specified headers and rows. * * @param input Table configuration including parent_uid, headers, and rows * @returns Result including success status and table block UID */ async createTable(input) { // Validate input const validation = validateTableInput(input); if (!validation.valid) { return { success: false, error: { code: 'VALIDATION_ERROR', message: validation.errors.map(e => `[${e.field}] ${e.message}`).join('; ') }, validation_passed: false, actions_attempted: 0 }; } // Build table actions const actions = buildTableActions(input); // Execute batch const result = await this.batchOps.processBatch(actions); // Add table_uid if successful if (result.success && result.uid_map) { return { ...result, table_uid: result.uid_map['table'] }; } return result; } }