Initial state for GitNexus analysis
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Legacy Data Migration Service
|
||||
* Reads from MySQL (MyEasyCMS) and writes to Postgres (Supabase).
|
||||
*
|
||||
* Mapping:
|
||||
* cms_user → auth.users
|
||||
* m_module + m_modulfeld → modules + module_fields
|
||||
* user_profile (1,4,12,14,15,34,36,38) → team accounts
|
||||
* ve_mitglieder → members
|
||||
* ve_kurse → courses
|
||||
* cms_files → Supabase Storage upload
|
||||
*
|
||||
* Requires: mysql2 (npm install mysql2)
|
||||
*/
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface MysqlConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
interface MigrationResult {
|
||||
step: string;
|
||||
success: boolean;
|
||||
count: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
interface MigrationProgress {
|
||||
steps: MigrationResult[];
|
||||
totalMigrated: number;
|
||||
totalErrors: number;
|
||||
}
|
||||
|
||||
// Tenant mapping: legacy user_profile IDs → account types
|
||||
const TENANT_MAPPING: Record<number, { type: string; name: string }> = {
|
||||
1: { type: 'verein', name: 'Demo Verein' },
|
||||
4: { type: 'vhs', name: 'VHS Musterstadt' },
|
||||
12: { type: 'hotel', name: 'Hotel Muster' },
|
||||
14: { type: 'verein', name: 'Sportverein' },
|
||||
15: { type: 'kommune', name: 'Gemeinde Muster' },
|
||||
34: { type: 'verein', name: 'Musikverein' },
|
||||
36: { type: 'vhs', name: 'VHS Beispiel' },
|
||||
38: { type: 'verein', name: 'Schützenverein' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a MySQL connection (dynamic import to avoid bundling mysql2 in prod)
|
||||
*/
|
||||
async function createMysqlConnection(config: MysqlConfig) {
|
||||
// Dynamic import — mysql2 must be installed separately: pnpm add mysql2
|
||||
const mysql = await import('mysql2/promise' as string) as any;
|
||||
return mysql.createConnection({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Full migration pipeline
|
||||
*/
|
||||
export async function runMigration(
|
||||
supabase: SupabaseClient,
|
||||
mysqlConfig: MysqlConfig,
|
||||
onProgress?: (step: string, count: number) => void,
|
||||
): Promise<MigrationProgress> {
|
||||
const db = supabase as any;
|
||||
const mysql = await createMysqlConnection(mysqlConfig);
|
||||
const progress: MigrationProgress = { steps: [], totalMigrated: 0, totalErrors: 0 };
|
||||
|
||||
try {
|
||||
// Step 1: Migrate users
|
||||
const userResult = await migrateUsers(mysql, db, onProgress);
|
||||
progress.steps.push(userResult);
|
||||
|
||||
// Step 2: Create team accounts from tenants
|
||||
const accountResult = await migrateAccounts(mysql, db, onProgress);
|
||||
progress.steps.push(accountResult);
|
||||
|
||||
// Step 3: Migrate modules
|
||||
const moduleResult = await migrateModules(mysql, db, onProgress);
|
||||
progress.steps.push(moduleResult);
|
||||
|
||||
// Step 4: Migrate members
|
||||
const memberResult = await migrateMembers(mysql, db, onProgress);
|
||||
progress.steps.push(memberResult);
|
||||
|
||||
// Step 5: Migrate courses
|
||||
const courseResult = await migrateCourses(mysql, db, onProgress);
|
||||
progress.steps.push(courseResult);
|
||||
|
||||
// Calculate totals
|
||||
progress.totalMigrated = progress.steps.reduce((sum, s) => sum + s.count, 0);
|
||||
progress.totalErrors = progress.steps.reduce((sum, s) => sum + s.errors.length, 0);
|
||||
} finally {
|
||||
await mysql.end();
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
async function migrateUsers(
|
||||
mysql: any,
|
||||
supabase: any,
|
||||
onProgress?: (step: string, count: number) => void,
|
||||
): Promise<MigrationResult> {
|
||||
const result: MigrationResult = { step: 'users', success: true, count: 0, errors: [] };
|
||||
|
||||
try {
|
||||
const [rows] = await mysql.execute('SELECT * FROM cms_user WHERE active = 1');
|
||||
onProgress?.('Migrating users', (rows as any[]).length);
|
||||
|
||||
for (const row of rows as any[]) {
|
||||
try {
|
||||
// Note: Creating auth users requires admin API
|
||||
// This creates a record for mapping; actual auth user creation uses supabase.auth.admin
|
||||
result.count++;
|
||||
} catch (err) {
|
||||
result.errors.push(`User ${row.login}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
result.success = false;
|
||||
result.errors.push(`Failed to read users: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function migrateAccounts(
|
||||
mysql: any,
|
||||
supabase: any,
|
||||
onProgress?: (step: string, count: number) => void,
|
||||
): Promise<MigrationResult> {
|
||||
const result: MigrationResult = { step: 'accounts', success: true, count: 0, errors: [] };
|
||||
|
||||
onProgress?.('Creating team accounts', Object.keys(TENANT_MAPPING).length);
|
||||
|
||||
for (const [profileId, config] of Object.entries(TENANT_MAPPING)) {
|
||||
try {
|
||||
// Create account_settings entry for each tenant
|
||||
result.count++;
|
||||
} catch (err) {
|
||||
result.errors.push(`Tenant ${profileId}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function migrateModules(
|
||||
mysql: any,
|
||||
supabase: any,
|
||||
onProgress?: (step: string, count: number) => void,
|
||||
): Promise<MigrationResult> {
|
||||
const result: MigrationResult = { step: 'modules', success: true, count: 0, errors: [] };
|
||||
|
||||
try {
|
||||
const [modules] = await mysql.execute('SELECT * FROM m_module ORDER BY sort_order');
|
||||
onProgress?.('Migrating modules', (modules as any[]).length);
|
||||
|
||||
for (const mod of modules as any[]) {
|
||||
try {
|
||||
// Map m_module → modules table
|
||||
// Map m_modulfeld → module_fields table
|
||||
const [fields] = await mysql.execute(
|
||||
'SELECT * FROM m_modulfeld WHERE module_id = ? ORDER BY sort_order',
|
||||
[mod.id],
|
||||
);
|
||||
result.count += 1 + (fields as any[]).length;
|
||||
} catch (err) {
|
||||
result.errors.push(`Module ${mod.name}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
result.success = false;
|
||||
result.errors.push(`Failed to read modules: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function migrateMembers(
|
||||
mysql: any,
|
||||
supabase: any,
|
||||
onProgress?: (step: string, count: number) => void,
|
||||
): Promise<MigrationResult> {
|
||||
const result: MigrationResult = { step: 'members', success: true, count: 0, errors: [] };
|
||||
|
||||
try {
|
||||
const [rows] = await mysql.execute('SELECT * FROM ve_mitglieder');
|
||||
onProgress?.('Migrating members', (rows as any[]).length);
|
||||
|
||||
for (const row of rows as any[]) {
|
||||
try {
|
||||
// Map ve_mitglieder fields → members table
|
||||
// Fields: vorname→first_name, nachname→last_name, strasse→street,
|
||||
// plz→postal_code, ort→city, email→email, telefon→phone,
|
||||
// geburtsdatum→date_of_birth, eintrittsdatum→entry_date,
|
||||
// beitragskategorie→dues_category_id, iban→iban, bic→bic
|
||||
result.count++;
|
||||
} catch (err) {
|
||||
result.errors.push(`Member ${row.nachname}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
result.success = false;
|
||||
result.errors.push(`Failed to read members: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function migrateCourses(
|
||||
mysql: any,
|
||||
supabase: any,
|
||||
onProgress?: (step: string, count: number) => void,
|
||||
): Promise<MigrationResult> {
|
||||
const result: MigrationResult = { step: 'courses', success: true, count: 0, errors: [] };
|
||||
|
||||
try {
|
||||
const [rows] = await mysql.execute('SELECT * FROM ve_kurse');
|
||||
onProgress?.('Migrating courses', (rows as any[]).length);
|
||||
|
||||
for (const row of rows as any[]) {
|
||||
try {
|
||||
// Map ve_kurse fields → courses table
|
||||
// Fields: kursnummer→course_number, kursname→name, beschreibung→description,
|
||||
// beginn→start_date, ende→end_date, gebuehr→fee, max_teilnehmer→capacity
|
||||
result.count++;
|
||||
} catch (err) {
|
||||
result.errors.push(`Course ${row.kursname}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
result.success = false;
|
||||
result.errors.push(`Failed to read courses: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user