Initial state for GitNexus analysis

This commit is contained in:
Zaid Marzguioui
2026-03-29 19:44:57 +02:00
parent 9d7c7f8030
commit 61ff48cb73
155 changed files with 23483 additions and 1722 deletions

View File

@@ -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;
}