export function createMysqlAdapter(context) { const { mkdir, dirname, sqliteSchema, mysqlSchema, optional, buildSeedOperations, stateFromRows, readSqliteRows, readMysqlRows, createRepository } = context; async function createMysqlStore({ seed }) { const { default: mysql } = await import('mysql2/promise'); const connectionUrl = process.env.DATABASE_URL; const database = process.env.MYSQL_DATABASE; if (!connectionUrl && (!process.env.MYSQL_HOST || !process.env.MYSQL_USER || !database)) { throw new Error('MySQL 配置不完整:请设置 DATABASE_URL,或 MYSQL_HOST、MYSQL_USER、MYSQL_DATABASE'); } const pool = connectionUrl ? mysql.createPool(connectionUrl) : mysql.createPool({ host: process.env.MYSQL_HOST, port: Number(process.env.MYSQL_PORT || 3306), user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD || '', database, waitForConnections: true, connectionLimit: Number(process.env.MYSQL_CONNECTION_LIMIT || 10), charset: 'utf8mb4', timezone: 'Z', enableKeepAlive: true }); for (const statement of mysqlSchema) await pool.execute(statement); const mysqlColumnMigrations = [ "ALTER TABLE users ADD COLUMN IF NOT EXISTS admin_level ENUM('super', 'school', 'class') NULL", 'ALTER TABLE users ADD COLUMN IF NOT EXISTS school_id VARCHAR(64) NULL', 'ALTER TABLE users ADD COLUMN IF NOT EXISTS class_id VARCHAR(64) NULL', 'ALTER TABLE users ADD COLUMN IF NOT EXISTS active BOOLEAN NOT NULL DEFAULT TRUE', 'ALTER TABLE users ADD COLUMN IF NOT EXISTS candidate_number VARCHAR(120) NULL', 'ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT FALSE', 'ALTER TABLE users ADD COLUMN IF NOT EXISTS archived_at VARCHAR(35) NULL', 'ALTER TABLE users ADD COLUMN IF NOT EXISTS archived_by VARCHAR(64) NULL', 'ALTER TABLE schema_metadata ADD COLUMN IF NOT EXISTS self_registration_enabled BOOLEAN NOT NULL DEFAULT FALSE', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS school_id VARCHAR(64) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS class_id VARCHAR(64) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS native_place VARCHAR(160) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS birth_date VARCHAR(20) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS ethnicity VARCHAR(60) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS postal_code VARCHAR(20) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS guardian_name VARCHAR(100) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS guardian_phone VARCHAR(60) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS profile_completed BOOLEAN NOT NULL DEFAULT FALSE', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS province_code VARCHAR(6) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS province_name VARCHAR(80) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS city_code VARCHAR(6) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS city_name VARCHAR(100) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS district_code VARCHAR(6) NULL', 'ALTER TABLE candidate_profiles ADD COLUMN IF NOT EXISTS district_name VARCHAR(100) NULL', 'ALTER TABLE registrations ADD COLUMN IF NOT EXISTS registration_number VARCHAR(120) NULL', 'ALTER TABLE registrations ADD COLUMN IF NOT EXISTS number_rule_id VARCHAR(64) NULL', "ALTER TABLE exams ADD COLUMN IF NOT EXISTS pass_policy ENUM('fixed_score', 'score_ratio', 'rank_percent', 'subject_scores', 'none') NOT NULL DEFAULT 'rank_percent'", 'ALTER TABLE exams ADD COLUMN IF NOT EXISTS pass_value DOUBLE NOT NULL DEFAULT 60', 'ALTER TABLE exams ADD COLUMN IF NOT EXISTS archived_at VARCHAR(35) NULL', 'ALTER TABLE exams ADD COLUMN IF NOT EXISTS archived_by VARCHAR(64) NULL', 'ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS full_score DOUBLE NOT NULL DEFAULT 150', 'ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS pass_score DOUBLE NOT NULL DEFAULT 90', "ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS pass_rule ENUM('fixed_score', 'score_ratio', 'none') NOT NULL DEFAULT 'fixed_score'", 'ALTER TABLE exam_subjects ADD COLUMN IF NOT EXISTS pass_value DOUBLE NOT NULL DEFAULT 90', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS code VARCHAR(40) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS manager_name VARCHAR(100) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS manager_phone VARCHAR(60) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS emergency_phone VARCHAR(60) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS gate_open_time VARCHAR(40) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS transport VARCHAR(500) NULL', "ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS status ENUM('active', 'inactive') NOT NULL DEFAULT 'active'", 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS notes VARCHAR(1000) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS province_code VARCHAR(6) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS province_name VARCHAR(80) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS city_code VARCHAR(6) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS city_name VARCHAR(100) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS district_code VARCHAR(6) NULL', 'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS district_name VARCHAR(100) NULL', 'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS province_code VARCHAR(6) NULL', 'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS province_name VARCHAR(80) NULL', 'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS city_code VARCHAR(6) NULL', 'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS city_name VARCHAR(100) NULL', 'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS district_code VARCHAR(6) NULL', 'ALTER TABLE center_change_requests ADD COLUMN IF NOT EXISTS district_name VARCHAR(100) NULL', 'ALTER TABLE test_rooms ADD COLUMN IF NOT EXISTS seat_plan VARCHAR(500) NULL', 'ALTER TABLE center_change_rooms ADD COLUMN IF NOT EXISTS seat_plan VARCHAR(500) NULL', "ALTER TABLE admit_cards ADD COLUMN IF NOT EXISTS center_code VARCHAR(40) NOT NULL DEFAULT ''", "ALTER TABLE admit_cards ADD COLUMN IF NOT EXISTS center_address VARCHAR(500) NOT NULL DEFAULT ''", "ALTER TABLE admit_card_subjects ADD COLUMN IF NOT EXISTS building VARCHAR(120) NOT NULL DEFAULT ''", "ALTER TABLE admit_card_subjects ADD COLUMN IF NOT EXISTS floor VARCHAR(80) NOT NULL DEFAULT ''", "ALTER TABLE workflow_definitions MODIFY COLUMN business_type ENUM('profile_change', 'registration_review', 'center_change', 'candidate_account_batch') NOT NULL" ]; for (const statement of mysqlColumnMigrations) await pool.execute(statement); const [resultLockTriggers] = await pool.execute(` SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = DATABASE() AND TRIGGER_NAME LIKE 'trg_results_lock_archived_%' `); const existingResultLockTriggers = new Set(resultLockTriggers.map(item => item.TRIGGER_NAME)); const mysqlResultLockTriggers = { trg_results_lock_archived_insert: `CREATE TRIGGER trg_results_lock_archived_insert BEFORE INSERT ON results FOR EACH ROW BEGIN IF EXISTS (SELECT 1 FROM registrations registration JOIN exams exam ON exam.id = registration.exam_id WHERE registration.id = NEW.registration_id AND exam.archived_at IS NOT NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '归档考试成绩已永久锁定'; END IF; END`, trg_results_lock_archived_update: `CREATE TRIGGER trg_results_lock_archived_update BEFORE UPDATE ON results FOR EACH ROW BEGIN IF EXISTS (SELECT 1 FROM registrations registration JOIN exams exam ON exam.id = registration.exam_id WHERE registration.id IN (OLD.registration_id, NEW.registration_id) AND exam.archived_at IS NOT NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '归档考试成绩已永久锁定'; END IF; END`, trg_results_lock_archived_delete: `CREATE TRIGGER trg_results_lock_archived_delete BEFORE DELETE ON results FOR EACH ROW BEGIN IF EXISTS (SELECT 1 FROM registrations registration JOIN exams exam ON exam.id = registration.exam_id WHERE registration.id = OLD.registration_id AND exam.archived_at IS NOT NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '归档考试成绩已永久锁定'; END IF; END` }; for (const [name, statement] of Object.entries(mysqlResultLockTriggers)) { if (!existingResultLockTriggers.has(name)) await pool.execute(statement); } const [legacyRegistrationNumberIndexes] = await pool.execute("SHOW INDEX FROM registrations WHERE Key_name = 'uq_registrations_number'"); if (legacyRegistrationNumberIndexes.length) await pool.execute('ALTER TABLE registrations DROP INDEX uq_registrations_number'); const [existing] = await pool.execute('SELECT id FROM schema_metadata WHERE id = 1'); if (existing.length) { const [metadataRows] = await pool.execute('SELECT app_version, schema_version FROM schema_metadata WHERE id = 1'); if (Number(metadataRows[0]?.schema_version || 1) < 9) { await pool.execute('DROP TABLE IF EXISTS admit_card_subjects'); await pool.execute('DROP TABLE IF EXISTS admit_cards'); await pool.execute('DROP TABLE IF EXISTS exam_arrangement_plans'); await pool.execute('DROP TABLE IF EXISTS admission_number_rules'); const admissionTables = ['admission_number_rules', 'exam_arrangement_plans', 'admit_cards', 'admit_card_subjects']; for (const table of admissionTables) { const statement = mysqlSchema.find(item => item.includes(`CREATE TABLE IF NOT EXISTS ${table} (`)); if (!statement) throw new Error(`缺少 ${table} 的 MySQL 表定义`); await pool.execute(statement); } const extension = seed(); for (const rule of extension.admissionNumberRules) await pool.execute( `INSERT INTO admission_number_rules ( id, code, name, description, separator, segments_json, example, active, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [rule.id, rule.code, rule.name, rule.description, rule.separator || '', JSON.stringify(rule.segments || []), rule.example || '', rule.active === false ? 0 : 1, rule.createdAt] ); await pool.execute('UPDATE schema_metadata SET schema_version = 9, app_version = 9 WHERE id = 1'); metadataRows[0].schema_version = 9; metadataRows[0].app_version = 9; } if (Number(metadataRows[0]?.schema_version || 1) < 10) { await pool.execute(`UPDATE admit_cards card LEFT JOIN test_centers center ON center.id = card.center_id SET card.center_code = COALESCE(center.code, card.center_code), card.center_address = COALESCE(CONCAT_WS(' ', center.province_name, center.city_name, center.district_name, center.address), card.center_address)`); await pool.execute(`UPDATE admit_card_subjects assignment LEFT JOIN test_rooms room ON room.id = assignment.room_id SET assignment.building = COALESCE(room.building, assignment.building), assignment.floor = COALESCE(room.floor, assignment.floor)`); await pool.execute('UPDATE schema_metadata SET schema_version = 10, app_version = 10 WHERE id = 1'); metadataRows[0].schema_version = 10; metadataRows[0].app_version = 10; } if (Number(metadataRows[0]?.schema_version || 1) < 11) { await pool.execute("UPDATE exam_subjects SET pass_rule = 'fixed_score', pass_value = pass_score"); await pool.execute('UPDATE schema_metadata SET schema_version = 11, app_version = 11 WHERE id = 1'); metadataRows[0].schema_version = 11; metadataRows[0].app_version = 11; } if (Number(metadataRows[0]?.schema_version || 1) < 12) { await pool.execute("UPDATE exams SET pass_policy = 'rank_percent' WHERE pass_policy = 'score_ratio'"); await pool.execute('UPDATE schema_metadata SET schema_version = 12, app_version = 12 WHERE id = 1'); metadataRows[0].schema_version = 12; metadataRows[0].app_version = 12; } if (Number(metadataRows[0]?.schema_version || 1) < 13) { await pool.execute('UPDATE schema_metadata SET schema_version = 13, app_version = 13 WHERE id = 1'); metadataRows[0].schema_version = 13; metadataRows[0].app_version = 13; } if (Number(metadataRows[0]?.schema_version || 1) < 14) { await pool.execute('UPDATE schema_metadata SET schema_version = 14, app_version = 14 WHERE id = 1'); metadataRows[0].schema_version = 14; metadataRows[0].app_version = 14; } if (Number(metadataRows[0]?.app_version || 1) < 2) { const extension = seed(); const connection = await pool.getConnection(); try { await connection.beginTransaction(); for (const school of extension.schools) await connection.execute( 'INSERT IGNORE INTO schools (id, name, code, address, active) VALUES (?, ?, ?, ?, ?)', [school.id, school.name, school.code, optional(school.address), school.active === false ? 0 : 1] ); for (const schoolClass of extension.classes) await connection.execute( 'INSERT IGNORE INTO school_classes (id, school_id, name, grade, active) VALUES (?, ?, ?, ?, ?)', [schoolClass.id, schoolClass.schoolId, schoolClass.name, schoolClass.grade, schoolClass.active === false ? 0 : 1] ); await connection.execute("UPDATE users SET admin_level = COALESCE(admin_level, 'super'), active = COALESCE(active, TRUE) WHERE role = 'admin'"); for (const user of extension.users.filter(item => item.role === 'admin')) await connection.execute( `INSERT IGNORE INTO users ( id, username, password_hash, role, admin_level, school_id, class_id, active, display_name, created_at ) VALUES (?, ?, ?, 'admin', ?, ?, ?, ?, ?, ?)`, [user.id, user.username, user.passwordHash, user.adminLevel, optional(user.schoolId), optional(user.classId), user.active === false ? 0 : 1, user.displayName, user.createdAt] ); for (const profile of extension.candidateProfiles) await connection.execute( `UPDATE candidate_profiles SET school_id = COALESCE(school_id, ?), class_id = COALESCE(class_id, ?) WHERE school = ? AND grade = ?`, [optional(profile.schoolId), optional(profile.classId), profile.school, profile.grade] ); const [centerRows] = await connection.execute('SELECT id FROM test_centers LIMIT 1'); if (!centerRows.length) for (const center of extension.testCenters) await connection.execute( 'INSERT INTO test_centers (id, school_id, name, address, contact, rooms, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [center.id, center.schoolId, center.name, center.address, optional(center.contact), center.rooms || '', center.updatedAt] ); const [ruleRows] = await connection.execute('SELECT id FROM number_rules LIMIT 1'); if (!ruleRows.length) for (const rule of extension.numberRules) { await connection.execute( 'INSERT INTO number_rules (id, name, separator, active, created_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [rule.id, rule.name, rule.separator || '', rule.active ? 1 : 0, optional(rule.createdBy), rule.updatedAt] ); for (const [index, segment] of rule.segments.entries()) await connection.execute( 'INSERT INTO number_rule_segments (id, rule_id, position, type, value, width) VALUES (?, ?, ?, ?, ?, ?)', [segment.id, rule.id, Number(segment.position || index + 1), segment.type, optional(segment.value), Number(segment.width || 0)] ); } const [workflowRows] = await connection.execute('SELECT id FROM workflow_definitions LIMIT 1'); if (!workflowRows.length) for (const workflow of extension.workflows) { await connection.execute( 'INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [workflow.id, workflow.businessType, workflow.name, workflow.active === false ? 0 : 1, optional(workflow.updatedBy), workflow.updatedAt] ); for (const [index, step] of workflow.steps.entries()) await connection.execute( 'INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level) VALUES (?, ?, ?, ?, ?)', [step.id, workflow.id, Number(step.position || index + 1), step.name, step.adminLevel] ); } await connection.execute('UPDATE schema_metadata SET schema_version = 2, app_version = 2 WHERE id = 1'); await connection.commit(); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } } if (Number(metadataRows[0]?.app_version || 1) < 3) { const extension = seed(); const connection = await pool.getConnection(); try { await connection.beginTransaction(); for (const center of extension.testCenters) await connection.execute( `UPDATE test_centers SET code = COALESCE(NULLIF(code, ''), ?), manager_name = COALESCE(manager_name, ?), manager_phone = COALESCE(manager_phone, ?), emergency_phone = COALESCE(emergency_phone, ?), gate_open_time = COALESCE(gate_open_time, ?), transport = COALESCE(transport, ?), status = COALESCE(status, 'active'), notes = COALESCE(notes, ?) WHERE id = ?`, [center.code, optional(center.managerName), optional(center.managerPhone), optional(center.emergencyPhone), optional(center.gateOpenTime), optional(center.transport), optional(center.notes), center.id] ); await connection.execute("UPDATE test_centers SET code = CONCAT('CENTER-', RIGHT(id, 8)) WHERE code IS NULL OR code = ''"); const [roomRows] = await connection.execute('SELECT id FROM test_rooms LIMIT 1'); if (!roomRows.length) for (const room of extension.testRooms) await connection.execute( `INSERT INTO test_rooms ( id, center_id, code, name, building, floor, capacity, seat_start, seat_end, room_type, status, notes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [room.id, room.centerId, room.code, room.name, room.building, optional(room.floor), Number(room.capacity), Number(room.seatStart), Number(room.seatEnd), room.roomType, room.status, optional(room.notes)] ); const centerWorkflow = extension.workflows.find(item => item.businessType === 'center_change'); const [centerWorkflowRows] = await connection.execute("SELECT id FROM workflow_definitions WHERE business_type = 'center_change' AND active = 1"); if (centerWorkflow && !centerWorkflowRows.length) { await connection.execute( 'INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [centerWorkflow.id, centerWorkflow.businessType, centerWorkflow.name, 1, optional(centerWorkflow.updatedBy), centerWorkflow.updatedAt] ); for (const [index, step] of centerWorkflow.steps.entries()) await connection.execute( 'INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level) VALUES (?, ?, ?, ?, ?)', [step.id, centerWorkflow.id, Number(step.position || index + 1), step.name, step.adminLevel] ); } await connection.execute('UPDATE schema_metadata SET schema_version = 3, app_version = 3 WHERE id = 1'); await connection.commit(); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } } if (Number(metadataRows[0]?.app_version || 1) < 4) { const extension = seed(); const connection = await pool.getConnection(); try { await connection.beginTransaction(); for (const user of extension.users.filter(item => item.role === 'candidate')) await connection.execute( `UPDATE users SET candidate_number = COALESCE(NULLIF(candidate_number, ''), ?), must_change_password = COALESCE(must_change_password, ?) WHERE id = ?`, [optional(user.candidateNumber), user.mustChangePassword ? 1 : 0, user.id] ); await connection.execute(`UPDATE users SET candidate_number = COALESCE( (SELECT registration_number FROM registrations WHERE registrations.user_id = users.id AND registration_number IS NOT NULL AND registration_number <> '' ORDER BY created_at LIMIT 1), CONCAT('CAND-', RIGHT(id, 10)) ) WHERE role = 'candidate' AND (candidate_number IS NULL OR candidate_number = '')`); for (const profile of extension.candidateProfiles) await connection.execute( `UPDATE candidate_profiles SET native_place = COALESCE(native_place, ?), birth_date = COALESCE(birth_date, ?), ethnicity = COALESCE(ethnicity, ?), postal_code = COALESCE(postal_code, ?), guardian_name = COALESCE(guardian_name, ?), guardian_phone = COALESCE(guardian_phone, ?), profile_completed = ? WHERE id = ?`, [optional(profile.nativePlace), optional(profile.birthDate), optional(profile.ethnicity), optional(profile.postalCode), optional(profile.guardianName), optional(profile.guardianPhone), profile.profileCompleted ? 1 : 0, profile.id] ); await connection.execute(`UPDATE registrations JOIN users ON users.id = registrations.user_id SET registrations.registration_number = users.candidate_number WHERE registrations.registration_number IS NULL OR registrations.registration_number = ''`); await connection.execute('UPDATE schema_metadata SET schema_version = 4, app_version = 4 WHERE id = 1'); await connection.commit(); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } } if (Number(metadataRows[0]?.app_version || 1) < 5) { const extension = seed(); const batchWorkflow = extension.workflows.find(item => item.businessType === 'candidate_account_batch'); const connection = await pool.getConnection(); try { await connection.beginTransaction(); const [batchWorkflowRows] = await connection.execute("SELECT id FROM workflow_definitions WHERE business_type = 'candidate_account_batch' AND active = 1"); if (batchWorkflow && !batchWorkflowRows.length) { await connection.execute( 'INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [batchWorkflow.id, batchWorkflow.businessType, batchWorkflow.name, 1, optional(batchWorkflow.updatedBy), batchWorkflow.updatedAt] ); for (const [index, step] of batchWorkflow.steps.entries()) await connection.execute( 'INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level) VALUES (?, ?, ?, ?, ?)', [step.id, batchWorkflow.id, Number(step.position || index + 1), step.name, step.adminLevel] ); } await connection.execute('UPDATE schema_metadata SET schema_version = 5, app_version = 5 WHERE id = 1'); await connection.commit(); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } } if (Number(metadataRows[0]?.app_version || 1) < 6) { await pool.execute('UPDATE schema_metadata SET schema_version = 6, app_version = 6 WHERE id = 1'); } if (Number(metadataRows[0]?.app_version || 1) < 7) { await pool.execute('UPDATE schema_metadata SET schema_version = 7, app_version = 7 WHERE id = 1'); } if (Number(metadataRows[0]?.schema_version || 1) < 14) { await pool.execute('UPDATE schema_metadata SET schema_version = 14, app_version = 14 WHERE id = 1'); } } if (!existing.length) { const initialState = seed(); const connection = await pool.getConnection(); try { await connection.beginTransaction(); const [insert] = await connection.execute(` INSERT IGNORE INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at) VALUES (1, 14, ?, ?, ?) `, [Number(initialState.meta?.version || 1), initialState.settings?.selfRegistrationEnabled ? 1 : 0, initialState.meta?.createdAt || new Date().toISOString()]); if (insert.affectedRows === 1) { for (const item of buildSeedOperations(initialState)) await connection.execute(item.sql, item.params); } await connection.commit(); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } } const [centerCodeIndexes] = await pool.execute("SHOW INDEX FROM test_centers WHERE Key_name = 'uq_centers_code'"); if (!centerCodeIndexes.length) { await pool.execute('ALTER TABLE test_centers MODIFY COLUMN code VARCHAR(40) NOT NULL, ADD UNIQUE KEY uq_centers_code (code)'); } const [candidateNumberIndexes] = await pool.execute("SHOW INDEX FROM users WHERE Key_name = 'uq_users_candidate_number'"); if (!candidateNumberIndexes.length) { await pool.execute('ALTER TABLE users ADD UNIQUE KEY uq_users_candidate_number (candidate_number)'); } const transaction = async operations => { const connection = await pool.getConnection(); try { await connection.beginTransaction(); for (const item of operations) await connection.execute(item.sql, item.params); await connection.commit(); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } }; const read = async () => { const connection = await pool.getConnection(); try { await connection.beginTransaction(); const state = stateFromRows(await readMysqlRows(connection)); await connection.commit(); return state; } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } }; return createRepository({ client: 'mysql', location: connectionUrl ? 'DATABASE_URL' : `${process.env.MYSQL_HOST}:${process.env.MYSQL_PORT || 3306}/${database}`, read, transaction, close: async () => pool.end() }); } return createMysqlStore; }