rspace-online/scripts/migrate-rtime-tasks.ts

166 lines
5.6 KiB
TypeScript

/**
* Migration: rTime TasksDoc → rTasks BoardDoc + WeavingDoc
*
* Reads all {space}:rtime:tasks docs and:
* 1. Creates TaskItem entries in the rTasks board (reusing Task.id)
* 2. Creates WeavingOverlay entries in the WeavingDoc
* 3. Moves connections + execStates from TasksDoc → WeavingDoc
*
* Usage: npx tsx scripts/migrate-rtime-tasks.ts [space]
* Default space: demo
*
* This script must be run on the server where Automerge data lives.
* It operates on the SyncServer data files directly.
*/
import * as Automerge from '@automerge/automerge';
import type { SyncServer } from '../server/local-first/sync-server';
import {
tasksDocId, weavingDocId, weavingSchema,
} from '../modules/rtime/schemas';
import type { TasksDoc, WeavingDoc, Task, Connection, ExecState } from '../modules/rtime/schemas';
import { boardDocId, createTaskItem, boardSchema } from '../modules/rtasks/schemas';
import type { BoardDoc, TaskItem } from '../modules/rtasks/schemas';
const space = process.argv[2] || 'demo';
console.log(`[migrate] Starting rTime → rTasks migration for space: ${space}`);
/**
* Standalone migration logic. Call with a SyncServer instance.
* This is exported so it can also be called from server startup if needed.
*/
export function migrateRTimeTasks(syncServer: SyncServer, spaceSlug: string): {
tasksCreated: number;
overlaysCreated: number;
connectionsMoved: number;
execStatesMoved: number;
} {
const result = { tasksCreated: 0, overlaysCreated: 0, connectionsMoved: 0, execStatesMoved: 0 };
// 1. Read legacy TasksDoc
const oldDocId = tasksDocId(spaceSlug);
const oldDoc = syncServer.getDoc<TasksDoc>(oldDocId);
if (!oldDoc) {
console.log(`[migrate] No TasksDoc found at ${oldDocId}, nothing to migrate.`);
return result;
}
const oldTasks = Object.values(oldDoc.tasks || {});
const oldConnections = Object.values(oldDoc.connections || {});
const oldExecStates = Object.values(oldDoc.execStates || {});
if (oldTasks.length === 0 && oldConnections.length === 0) {
console.log(`[migrate] TasksDoc is empty, nothing to migrate.`);
return result;
}
console.log(`[migrate] Found ${oldTasks.length} tasks, ${oldConnections.length} connections, ${oldExecStates.length} exec states`);
// 2. Ensure rTasks board exists
const bDocId = boardDocId(spaceSlug, spaceSlug);
let boardDoc = syncServer.getDoc<BoardDoc>(bDocId);
if (!boardDoc) {
boardDoc = Automerge.change(Automerge.init<BoardDoc>(), 'init board for migration', (d) => {
const init = boardSchema.init();
Object.assign(d, init);
d.meta.spaceSlug = spaceSlug;
d.board.id = spaceSlug;
d.board.slug = spaceSlug;
d.board.name = `${spaceSlug} Board`;
});
syncServer.setDoc(bDocId, boardDoc);
console.log(`[migrate] Created rTasks board: ${bDocId}`);
}
// 3. Create TaskItem for each rTime Task (reuse ID)
for (const task of oldTasks) {
if (boardDoc.tasks[task.id]) {
console.log(`[migrate] Task ${task.id} already exists in board, skipping.`);
continue;
}
const taskItem = createTaskItem(task.id, spaceSlug, task.name, {
description: task.description || '',
});
syncServer.changeDoc<BoardDoc>(bDocId, `migrate task: ${task.name}`, (d) => {
d.tasks[task.id] = taskItem as any;
});
result.tasksCreated++;
}
// 4. Ensure WeavingDoc exists
const wDocId = weavingDocId(spaceSlug);
let wDoc = syncServer.getDoc<WeavingDoc>(wDocId);
if (!wDoc) {
wDoc = Automerge.change(Automerge.init<WeavingDoc>(), 'init weaving for migration', (d) => {
const init = weavingSchema.init();
Object.assign(d, init);
d.meta.spaceSlug = spaceSlug;
d.boardSlug = spaceSlug;
});
syncServer.setDoc(wDocId, wDoc);
}
// 5. Create WeavingOverlay for each task (with needs, notes, links)
let canvasX = 300;
for (const task of oldTasks) {
if (wDoc.weavingOverlays[task.id]) {
console.log(`[migrate] Overlay for ${task.id} already exists, skipping.`);
continue;
}
syncServer.changeDoc<WeavingDoc>(wDocId, `migrate overlay: ${task.name}`, (d) => {
d.weavingOverlays[task.id] = {
rtasksId: task.id,
needs: task.needs || {},
canvasX,
canvasY: 150,
notes: task.notes || '',
links: task.links || [],
intentFrameId: task.intentFrameId,
} as any;
});
canvasX += 280;
result.overlaysCreated++;
}
// 6. Move connections from TasksDoc → WeavingDoc
for (const conn of oldConnections) {
syncServer.changeDoc<WeavingDoc>(wDocId, `migrate connection: ${conn.id}`, (d) => {
if (!d.connections[conn.id]) {
d.connections[conn.id] = { ...conn } as any;
result.connectionsMoved++;
}
});
}
// 7. Move execStates from TasksDoc → WeavingDoc
for (const es of oldExecStates) {
syncServer.changeDoc<WeavingDoc>(wDocId, `migrate exec state: ${es.taskId}`, (d) => {
if (!d.execStates[es.taskId]) {
d.execStates[es.taskId] = { ...es } as any;
result.execStatesMoved++;
}
});
}
// Refresh doc after changes
const finalW = syncServer.getDoc<WeavingDoc>(wDocId)!;
result.connectionsMoved = Object.keys(finalW.connections).length;
result.execStatesMoved = Object.keys(finalW.execStates).length;
console.log(`[migrate] Migration complete:
Tasks created in rTasks: ${result.tasksCreated}
Weaving overlays created: ${result.overlaysCreated}
Connections moved: ${result.connectionsMoved}
Exec states moved: ${result.execStatesMoved}`);
return result;
}
// CLI entry point — only runs when executed directly
if (process.argv[1]?.includes('migrate-rtime-tasks')) {
console.log('[migrate] This script must be imported and called with a SyncServer instance.');
console.log('[migrate] Example: import { migrateRTimeTasks } from "./scripts/migrate-rtime-tasks";');
console.log('[migrate] migrateRTimeTasks(syncServer, "demo");');
}