439 lines
27 KiB
JavaScript
439 lines
27 KiB
JavaScript
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;
|
|
}
|