459 lines
16 KiB
JavaScript
459 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* migrate-data.js — One-time migration from SQLite to NocoDB.
|
|
* Run: node migrate-data.js
|
|
*/
|
|
|
|
require('dotenv').config({ path: __dirname + '/.env' });
|
|
const Database = require('better-sqlite3');
|
|
const bcrypt = require('bcrypt');
|
|
const path = require('path');
|
|
const nocodb = require('./nocodb');
|
|
const { authDb } = require('./auth-db');
|
|
|
|
const sqliteDb = new Database(path.join(__dirname, 'marketing.db'), { readonly: true });
|
|
|
|
// ID mapping: { tableName: { oldId: newNocoDbId } }
|
|
const idMap = {};
|
|
function mapId(table, oldId, newId) {
|
|
if (!idMap[table]) idMap[table] = {};
|
|
idMap[table][oldId] = newId;
|
|
}
|
|
function getMappedId(table, oldId) {
|
|
if (!oldId) return null;
|
|
return idMap[table]?.[oldId] || null;
|
|
}
|
|
|
|
// Build a lookup: team_member_id → user row
|
|
function buildTeamMemberToUserMap() {
|
|
const users = sqliteDb.prepare('SELECT id, team_member_id FROM users').all();
|
|
const map = {};
|
|
for (const u of users) {
|
|
if (u.team_member_id) map[u.team_member_id] = u.id;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
async function migrateBrands() {
|
|
console.log('Migrating Brands...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM brands ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Brands', {
|
|
name: row.name,
|
|
priority: row.priority,
|
|
color: row.color,
|
|
icon: row.icon,
|
|
});
|
|
mapId('brands', row.id, created.Id);
|
|
}
|
|
console.log(` ✅ ${rows.length} brands migrated`);
|
|
}
|
|
|
|
async function migrateUsers() {
|
|
console.log('Migrating Users...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM users ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Users', {
|
|
name: row.name,
|
|
email: row.email,
|
|
role: row.role,
|
|
team_role: row.team_role || null,
|
|
brands: row.brands || '[]',
|
|
phone: row.phone || null,
|
|
avatar: row.avatar || null,
|
|
tutorial_completed: !!row.tutorial_completed,
|
|
});
|
|
mapId('users', row.id, created.Id);
|
|
|
|
// Create auth credentials entry
|
|
authDb.prepare(
|
|
'INSERT OR REPLACE INTO auth_credentials (email, password_hash, nocodb_user_id) VALUES (?, ?, ?)'
|
|
).run(row.email, row.password_hash, created.Id);
|
|
}
|
|
console.log(` ✅ ${rows.length} users migrated (+ auth_credentials)`);
|
|
}
|
|
|
|
async function migrateCampaigns() {
|
|
console.log('Migrating Campaigns...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM campaigns ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const data = {
|
|
name: row.name,
|
|
description: row.description,
|
|
start_date: row.start_date,
|
|
end_date: row.end_date,
|
|
status: row.status,
|
|
color: row.color,
|
|
budget: row.budget,
|
|
goals: row.goals,
|
|
platforms: row.platforms || '[]',
|
|
budget_spent: row.budget_spent || 0,
|
|
revenue: row.revenue || 0,
|
|
impressions: row.impressions || 0,
|
|
clicks: row.clicks || 0,
|
|
conversions: row.conversions || 0,
|
|
cost_per_click: row.cost_per_click || 0,
|
|
notes: row.notes || '',
|
|
};
|
|
|
|
const created = await nocodb.create('Campaigns', data);
|
|
mapId('campaigns', row.id, created.Id);
|
|
|
|
// Link Brand
|
|
if (row.brand_id && getMappedId('brands', row.brand_id)) {
|
|
await linkRecord('Campaigns', created.Id, 'Brand', getMappedId('brands', row.brand_id));
|
|
}
|
|
// Link CreatedByUser
|
|
if (row.created_by_user_id && getMappedId('users', row.created_by_user_id)) {
|
|
await linkRecord('Campaigns', created.Id, 'CreatedByUser', getMappedId('users', row.created_by_user_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} campaigns migrated`);
|
|
}
|
|
|
|
async function migrateCampaignTracks() {
|
|
console.log('Migrating CampaignTracks...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM campaign_tracks ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('CampaignTracks', {
|
|
name: row.name,
|
|
type: row.type,
|
|
platform: row.platform,
|
|
budget_allocated: row.budget_allocated || 0,
|
|
budget_spent: row.budget_spent || 0,
|
|
revenue: row.revenue || 0,
|
|
impressions: row.impressions || 0,
|
|
clicks: row.clicks || 0,
|
|
conversions: row.conversions || 0,
|
|
notes: row.notes || '',
|
|
status: row.status,
|
|
});
|
|
mapId('campaign_tracks', row.id, created.Id);
|
|
|
|
if (row.campaign_id && getMappedId('campaigns', row.campaign_id)) {
|
|
await linkRecord('CampaignTracks', created.Id, 'Campaign', getMappedId('campaigns', row.campaign_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} campaign tracks migrated`);
|
|
}
|
|
|
|
async function migrateCampaignAssignments() {
|
|
console.log('Migrating CampaignAssignments...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM campaign_assignments ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('CampaignAssignments', {
|
|
assigned_at: row.assigned_at,
|
|
});
|
|
mapId('campaign_assignments', row.id, created.Id);
|
|
|
|
if (row.campaign_id && getMappedId('campaigns', row.campaign_id)) {
|
|
await linkRecord('CampaignAssignments', created.Id, 'Campaign', getMappedId('campaigns', row.campaign_id));
|
|
}
|
|
if (row.user_id && getMappedId('users', row.user_id)) {
|
|
await linkRecord('CampaignAssignments', created.Id, 'Member', getMappedId('users', row.user_id));
|
|
}
|
|
if (row.assigned_by && getMappedId('users', row.assigned_by)) {
|
|
await linkRecord('CampaignAssignments', created.Id, 'Assigner', getMappedId('users', row.assigned_by));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} campaign assignments migrated`);
|
|
}
|
|
|
|
async function migrateProjects() {
|
|
console.log('Migrating Projects...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM projects ORDER BY id').all();
|
|
const tmToUser = buildTeamMemberToUserMap();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Projects', {
|
|
name: row.name,
|
|
description: row.description,
|
|
status: row.status,
|
|
priority: row.priority,
|
|
due_date: row.due_date,
|
|
});
|
|
mapId('projects', row.id, created.Id);
|
|
|
|
if (row.brand_id && getMappedId('brands', row.brand_id)) {
|
|
await linkRecord('Projects', created.Id, 'Brand', getMappedId('brands', row.brand_id));
|
|
}
|
|
// owner_id references team_members, map through to users
|
|
if (row.owner_id) {
|
|
const userId = tmToUser[row.owner_id];
|
|
if (userId && getMappedId('users', userId)) {
|
|
await linkRecord('Projects', created.Id, 'Owner', getMappedId('users', userId));
|
|
}
|
|
}
|
|
if (row.created_by_user_id && getMappedId('users', row.created_by_user_id)) {
|
|
await linkRecord('Projects', created.Id, 'CreatedByUser', getMappedId('users', row.created_by_user_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} projects migrated`);
|
|
}
|
|
|
|
async function migrateTasks() {
|
|
console.log('Migrating Tasks...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM tasks ORDER BY id').all();
|
|
const tmToUser = buildTeamMemberToUserMap();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Tasks', {
|
|
title: row.title,
|
|
description: row.description,
|
|
status: row.status,
|
|
priority: row.priority,
|
|
due_date: row.due_date,
|
|
is_personal: !!row.is_personal,
|
|
completed_at: row.completed_at,
|
|
});
|
|
mapId('tasks', row.id, created.Id);
|
|
|
|
if (row.project_id && getMappedId('projects', row.project_id)) {
|
|
await linkRecord('Tasks', created.Id, 'Project', getMappedId('projects', row.project_id));
|
|
}
|
|
// assigned_to references team_members
|
|
if (row.assigned_to) {
|
|
const userId = tmToUser[row.assigned_to];
|
|
if (userId && getMappedId('users', userId)) {
|
|
await linkRecord('Tasks', created.Id, 'AssignedTo', getMappedId('users', userId));
|
|
}
|
|
}
|
|
if (row.created_by_user_id && getMappedId('users', row.created_by_user_id)) {
|
|
await linkRecord('Tasks', created.Id, 'CreatedByUser', getMappedId('users', row.created_by_user_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} tasks migrated`);
|
|
}
|
|
|
|
async function migratePosts() {
|
|
console.log('Migrating Posts...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM posts ORDER BY id').all();
|
|
const tmToUser = buildTeamMemberToUserMap();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Posts', {
|
|
title: row.title,
|
|
description: row.description,
|
|
status: row.status,
|
|
platform: row.platform,
|
|
platforms: row.platforms || '[]',
|
|
content_type: row.content_type,
|
|
scheduled_date: row.scheduled_date,
|
|
published_date: row.published_date,
|
|
notes: row.notes,
|
|
publication_links: row.publication_links || '[]',
|
|
});
|
|
mapId('posts', row.id, created.Id);
|
|
|
|
if (row.brand_id && getMappedId('brands', row.brand_id)) {
|
|
await linkRecord('Posts', created.Id, 'Brand', getMappedId('brands', row.brand_id));
|
|
}
|
|
if (row.assigned_to) {
|
|
const userId = tmToUser[row.assigned_to];
|
|
if (userId && getMappedId('users', userId)) {
|
|
await linkRecord('Posts', created.Id, 'AssignedTo', getMappedId('users', userId));
|
|
}
|
|
}
|
|
if (row.campaign_id && getMappedId('campaigns', row.campaign_id)) {
|
|
await linkRecord('Posts', created.Id, 'Campaign', getMappedId('campaigns', row.campaign_id));
|
|
}
|
|
if (row.track_id && getMappedId('campaign_tracks', row.track_id)) {
|
|
await linkRecord('Posts', created.Id, 'Track', getMappedId('campaign_tracks', row.track_id));
|
|
}
|
|
if (row.created_by_user_id && getMappedId('users', row.created_by_user_id)) {
|
|
await linkRecord('Posts', created.Id, 'CreatedByUser', getMappedId('users', row.created_by_user_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} posts migrated`);
|
|
}
|
|
|
|
async function migratePostAttachments() {
|
|
console.log('Migrating PostAttachments...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM post_attachments ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('PostAttachments', {
|
|
filename: row.filename,
|
|
original_name: row.original_name,
|
|
mime_type: row.mime_type,
|
|
size: row.size,
|
|
url: row.url,
|
|
});
|
|
mapId('post_attachments', row.id, created.Id);
|
|
|
|
if (row.post_id && getMappedId('posts', row.post_id)) {
|
|
await linkRecord('PostAttachments', created.Id, 'Post', getMappedId('posts', row.post_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} post attachments migrated`);
|
|
}
|
|
|
|
async function migrateAssets() {
|
|
console.log('Migrating Assets...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM assets ORDER BY id').all();
|
|
const tmToUser = buildTeamMemberToUserMap();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Assets', {
|
|
filename: row.filename,
|
|
original_name: row.original_name,
|
|
mime_type: row.mime_type,
|
|
size: row.size,
|
|
tags: row.tags || '[]',
|
|
folder: row.folder,
|
|
});
|
|
mapId('assets', row.id, created.Id);
|
|
|
|
if (row.brand_id && getMappedId('brands', row.brand_id)) {
|
|
await linkRecord('Assets', created.Id, 'Brand', getMappedId('brands', row.brand_id));
|
|
}
|
|
if (row.campaign_id && getMappedId('campaigns', row.campaign_id)) {
|
|
await linkRecord('Assets', created.Id, 'Campaign', getMappedId('campaigns', row.campaign_id));
|
|
}
|
|
if (row.uploaded_by) {
|
|
const userId = tmToUser[row.uploaded_by];
|
|
if (userId && getMappedId('users', userId)) {
|
|
await linkRecord('Assets', created.Id, 'Uploader', getMappedId('users', userId));
|
|
}
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} assets migrated`);
|
|
}
|
|
|
|
async function migrateComments() {
|
|
console.log('Migrating Comments...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM comments ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('Comments', {
|
|
entity_type: row.entity_type,
|
|
entity_id: row.entity_id,
|
|
content: row.content,
|
|
});
|
|
mapId('comments', row.id, created.Id);
|
|
|
|
if (row.user_id && getMappedId('users', row.user_id)) {
|
|
await linkRecord('Comments', created.Id, 'User', getMappedId('users', row.user_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} comments migrated`);
|
|
}
|
|
|
|
async function migrateBudgetEntries() {
|
|
console.log('Migrating BudgetEntries...');
|
|
const rows = sqliteDb.prepare('SELECT * FROM budget_entries ORDER BY id').all();
|
|
for (const row of rows) {
|
|
const created = await nocodb.create('BudgetEntries', {
|
|
label: row.label,
|
|
amount: row.amount,
|
|
source: row.source,
|
|
category: row.category,
|
|
date_received: row.date_received,
|
|
notes: row.notes || '',
|
|
});
|
|
mapId('budget_entries', row.id, created.Id);
|
|
|
|
if (row.campaign_id && getMappedId('campaigns', row.campaign_id)) {
|
|
await linkRecord('BudgetEntries', created.Id, 'Campaign', getMappedId('campaigns', row.campaign_id));
|
|
}
|
|
}
|
|
console.log(` ✅ ${rows.length} budget entries migrated`);
|
|
}
|
|
|
|
// Helper: link a record to another via NocoDB link API
|
|
async function linkRecord(table, recordId, linkField, linkedRecordId) {
|
|
try {
|
|
const tableId = await nocodb.resolveTableId(table);
|
|
// Get columns to find the link column ID
|
|
const res = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}`, {
|
|
headers: { 'xc-token': nocodb.token },
|
|
});
|
|
const tableMeta = await res.json();
|
|
const linkCol = tableMeta.columns.find(c => c.title === linkField && c.uidt === 'Links');
|
|
if (!linkCol) {
|
|
console.warn(` ⚠ Link column "${linkField}" not found in ${table}`);
|
|
return;
|
|
}
|
|
await fetch(`${nocodb.url}/api/v2/tables/${tableId}/links/${linkCol.id}/records/${recordId}`, {
|
|
method: 'POST',
|
|
headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify([{ Id: linkedRecordId }]),
|
|
});
|
|
} catch (err) {
|
|
console.warn(` ⚠ Failed to link ${table}.${linkField}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Cache for link column metadata (avoid re-fetching per record)
|
|
const linkColCache = {};
|
|
async function getLinkColId(table, linkField) {
|
|
const key = `${table}.${linkField}`;
|
|
if (linkColCache[key]) return linkColCache[key];
|
|
const tableId = await nocodb.resolveTableId(table);
|
|
const res = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}`, {
|
|
headers: { 'xc-token': nocodb.token },
|
|
});
|
|
const tableMeta = await res.json();
|
|
for (const c of tableMeta.columns) {
|
|
if (c.uidt === 'Links') {
|
|
linkColCache[`${table}.${c.title}`] = { colId: c.id, tableId };
|
|
}
|
|
}
|
|
return linkColCache[key] || null;
|
|
}
|
|
|
|
// Optimized linkRecord using cache
|
|
const origLinkRecord = linkRecord;
|
|
linkRecord = async function(table, recordId, linkField, linkedRecordId) {
|
|
let cached = linkColCache[`${table}.${linkField}`];
|
|
if (!cached) {
|
|
// Populate cache for this table
|
|
const tableId = await nocodb.resolveTableId(table);
|
|
const res = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}`, {
|
|
headers: { 'xc-token': nocodb.token },
|
|
});
|
|
const tableMeta = await res.json();
|
|
for (const c of tableMeta.columns) {
|
|
if (c.uidt === 'Links') {
|
|
linkColCache[`${table}.${c.title}`] = { colId: c.id, tableId };
|
|
}
|
|
}
|
|
cached = linkColCache[`${table}.${linkField}`];
|
|
}
|
|
if (!cached) {
|
|
console.warn(` ⚠ Link column "${linkField}" not found in ${table}`);
|
|
return;
|
|
}
|
|
try {
|
|
await fetch(`${nocodb.url}/api/v2/tables/${cached.tableId}/links/${cached.colId}/records/${recordId}`, {
|
|
method: 'POST',
|
|
headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify([{ Id: linkedRecordId }]),
|
|
});
|
|
} catch (err) {
|
|
console.warn(` ⚠ Failed to link ${table}.${linkField}: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
async function main() {
|
|
console.log('Starting migration from SQLite → NocoDB...\n');
|
|
|
|
await migrateBrands();
|
|
await migrateUsers();
|
|
await migrateCampaigns();
|
|
await migrateCampaignTracks();
|
|
await migrateCampaignAssignments();
|
|
await migrateProjects();
|
|
await migrateTasks();
|
|
await migratePosts();
|
|
await migratePostAttachments();
|
|
await migrateAssets();
|
|
await migrateComments();
|
|
await migrateBudgetEntries();
|
|
|
|
console.log('\n✅ Migration complete!');
|
|
console.log('ID mapping summary:');
|
|
for (const [table, map] of Object.entries(idMap)) {
|
|
console.log(` ${table}: ${Object.keys(map).length} records`);
|
|
}
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Migration failed:', err);
|
|
process.exit(1);
|
|
});
|