Implement fixed candidate account onboarding
This commit is contained in:
+206
-32
@@ -35,6 +35,7 @@ const sqliteSchema = `
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
app_version INTEGER NOT NULL DEFAULT 1,
|
||||
self_registration_enabled INTEGER NOT NULL DEFAULT 0 CHECK (self_registration_enabled IN (0, 1)),
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
@@ -66,12 +67,14 @@ const sqliteSchema = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
candidate_number TEXT UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin', 'candidate')),
|
||||
admin_level TEXT CHECK (admin_level IN ('super', 'school', 'class')),
|
||||
school_id TEXT REFERENCES schools(id) ON DELETE SET NULL,
|
||||
class_id TEXT REFERENCES school_classes(id) ON DELETE SET NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)),
|
||||
must_change_password INTEGER NOT NULL DEFAULT 0 CHECK (must_change_password IN (0, 1)),
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
@@ -91,6 +94,13 @@ const sqliteSchema = `
|
||||
address TEXT,
|
||||
emergency_contact TEXT,
|
||||
emergency_phone TEXT,
|
||||
native_place TEXT,
|
||||
birth_date TEXT,
|
||||
ethnicity TEXT,
|
||||
postal_code TEXT,
|
||||
guardian_name TEXT,
|
||||
guardian_phone TEXT,
|
||||
profile_completed INTEGER NOT NULL DEFAULT 0 CHECK (profile_completed IN (0, 1)),
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
review_note TEXT,
|
||||
reviewed_at TEXT,
|
||||
@@ -148,7 +158,7 @@ const sqliteSchema = `
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT,
|
||||
review_note TEXT,
|
||||
registration_number TEXT UNIQUE,
|
||||
registration_number TEXT,
|
||||
number_rule_id TEXT,
|
||||
UNIQUE (user_id, exam_id)
|
||||
) STRICT;
|
||||
@@ -344,6 +354,7 @@ const mysqlSchema = [
|
||||
id TINYINT UNSIGNED NOT NULL,
|
||||
schema_version INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
app_version INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
self_registration_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT chk_schema_metadata_singleton CHECK (id = 1)
|
||||
@@ -380,16 +391,19 @@ const mysqlSchema = [
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
candidate_number VARCHAR(120) NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin', 'candidate') NOT NULL,
|
||||
admin_level ENUM('super', 'school', 'class') NULL,
|
||||
school_id VARCHAR(64) NULL,
|
||||
class_id VARCHAR(64) NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
must_change_password BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_users_username (username),
|
||||
UNIQUE KEY uq_users_candidate_number (candidate_number),
|
||||
KEY idx_users_admin_scope (role, admin_level, school_id, class_id),
|
||||
CONSTRAINT fk_users_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_users_class FOREIGN KEY (class_id) REFERENCES school_classes(id) ON DELETE SET NULL
|
||||
@@ -409,6 +423,13 @@ const mysqlSchema = [
|
||||
address VARCHAR(255) NULL,
|
||||
emergency_contact VARCHAR(100) NULL,
|
||||
emergency_phone VARCHAR(60) NULL,
|
||||
native_place VARCHAR(160) NULL,
|
||||
birth_date VARCHAR(20) NULL,
|
||||
ethnicity VARCHAR(60) NULL,
|
||||
postal_code VARCHAR(20) NULL,
|
||||
guardian_name VARCHAR(100) NULL,
|
||||
guardian_phone VARCHAR(60) NULL,
|
||||
profile_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status ENUM('pending', 'approved', 'rejected') NOT NULL,
|
||||
review_note VARCHAR(500) NULL,
|
||||
reviewed_at VARCHAR(35) NULL,
|
||||
@@ -482,7 +503,6 @@ const mysqlSchema = [
|
||||
number_rule_id VARCHAR(64) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_registrations_user_exam (user_id, exam_id),
|
||||
UNIQUE KEY uq_registrations_number (registration_number),
|
||||
KEY idx_registrations_status (status),
|
||||
KEY idx_registrations_exam (exam_id),
|
||||
CONSTRAINT fk_registrations_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -710,8 +730,9 @@ function buildSeedOperations(state) {
|
||||
const nullable = value => value == null || value === '' ? null : value;
|
||||
|
||||
add(
|
||||
'UPDATE schema_metadata SET schema_version = 3, app_version = ?, created_at = ? WHERE id = 1',
|
||||
Number(state.meta?.version || 1), state.meta?.createdAt || new Date().toISOString()
|
||||
'UPDATE schema_metadata SET schema_version = 4, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
|
||||
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
|
||||
state.meta?.createdAt || new Date().toISOString()
|
||||
);
|
||||
add(
|
||||
'INSERT INTO organization (id, name, code, phone, address) VALUES (1, ?, ?, ?, ?)',
|
||||
@@ -735,10 +756,11 @@ function buildSeedOperations(state) {
|
||||
for (const user of state.users) {
|
||||
add(
|
||||
`INSERT INTO users (
|
||||
id, username, password_hash, role, admin_level, school_id, class_id, active, display_name, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
user.id, user.username, user.passwordHash, user.role, nullable(user.adminLevel), nullable(user.schoolId),
|
||||
nullable(user.classId), user.active === false ? 0 : 1, user.displayName, user.createdAt
|
||||
id, username, candidate_number, password_hash, role, admin_level, school_id, class_id, active,
|
||||
must_change_password, display_name, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
user.id, user.username, nullable(user.candidateNumber), user.passwordHash, user.role, nullable(user.adminLevel), nullable(user.schoolId),
|
||||
nullable(user.classId), user.active === false ? 0 : 1, user.mustChangePassword ? 1 : 0, user.displayName, user.createdAt
|
||||
);
|
||||
}
|
||||
|
||||
@@ -746,12 +768,15 @@ function buildSeedOperations(state) {
|
||||
add(
|
||||
`INSERT INTO candidate_profiles (
|
||||
id, user_id, name, gender, id_number, phone, email, school, grade, school_id, class_id, address,
|
||||
emergency_contact, emergency_phone, status, review_note, reviewed_at, reviewer_id, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
emergency_contact, emergency_phone, native_place, birth_date, ethnicity, postal_code, guardian_name,
|
||||
guardian_phone, profile_completed, status, review_note, reviewed_at, reviewer_id, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
profile.id, profile.userId, profile.name, nullable(profile.gender), profile.idNumber, profile.phone,
|
||||
nullable(profile.email), nullable(profile.school), nullable(profile.grade), nullable(profile.schoolId),
|
||||
nullable(profile.classId), nullable(profile.address),
|
||||
nullable(profile.emergencyContact), nullable(profile.emergencyPhone), profile.status, nullable(profile.reviewNote),
|
||||
nullable(profile.emergencyContact), nullable(profile.emergencyPhone), nullable(profile.nativePlace), nullable(profile.birthDate),
|
||||
nullable(profile.ethnicity), nullable(profile.postalCode), nullable(profile.guardianName), nullable(profile.guardianPhone),
|
||||
profile.profileCompleted ? 1 : 0, profile.status, nullable(profile.reviewNote),
|
||||
nullable(profile.reviewedAt), nullable(profile.reviewerId), profile.updatedAt
|
||||
);
|
||||
}
|
||||
@@ -982,6 +1007,7 @@ function stateFromRows(rows) {
|
||||
const organization = rows.organization;
|
||||
const state = {
|
||||
meta: { version: Number(rows.system.app_version), createdAt: rows.system.created_at },
|
||||
settings: { selfRegistrationEnabled: Boolean(rows.system.self_registration_enabled) },
|
||||
organization: {
|
||||
name: organization.name,
|
||||
code: organization.code,
|
||||
@@ -1005,12 +1031,14 @@ function stateFromRows(rows) {
|
||||
users: rows.users.map(row => ({
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
candidateNumber: row.candidate_number || '',
|
||||
passwordHash: row.password_hash,
|
||||
role: row.role,
|
||||
adminLevel: row.admin_level || (row.role === 'admin' ? 'super' : null),
|
||||
schoolId: row.school_id || null,
|
||||
classId: row.class_id || null,
|
||||
active: row.active == null ? true : Boolean(row.active),
|
||||
mustChangePassword: Boolean(row.must_change_password),
|
||||
displayName: row.display_name,
|
||||
createdAt: row.created_at
|
||||
})),
|
||||
@@ -1029,6 +1057,13 @@ function stateFromRows(rows) {
|
||||
address: row.address || '',
|
||||
emergencyContact: row.emergency_contact || '',
|
||||
emergencyPhone: row.emergency_phone || '',
|
||||
nativePlace: row.native_place || '',
|
||||
birthDate: row.birth_date || '',
|
||||
ethnicity: row.ethnicity || '',
|
||||
postalCode: row.postal_code || '',
|
||||
guardianName: row.guardian_name || '',
|
||||
guardianPhone: row.guardian_phone || '',
|
||||
profileCompleted: Boolean(row.profile_completed),
|
||||
status: row.status,
|
||||
reviewNote: row.review_note || '',
|
||||
reviewedAt: row.reviewed_at,
|
||||
@@ -1310,28 +1345,33 @@ function createRepository({ client, location, read, transaction, close }) {
|
||||
location,
|
||||
read,
|
||||
close,
|
||||
async createCandidate(user, profile, instance, action) {
|
||||
async createCandidate(user, profile, instance, action, log = null) {
|
||||
const operations = [
|
||||
operation(
|
||||
`INSERT INTO users (
|
||||
id, username, password_hash, role, admin_level, school_id, class_id, active, display_name, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
user.id, user.username, user.passwordHash, user.role, optional(user.adminLevel), optional(user.schoolId),
|
||||
optional(user.classId), user.active === false ? 0 : 1, user.displayName, user.createdAt
|
||||
id, username, candidate_number, password_hash, role, admin_level, school_id, class_id, active,
|
||||
must_change_password, display_name, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
user.id, user.username, optional(user.candidateNumber), user.passwordHash, user.role, optional(user.adminLevel), optional(user.schoolId),
|
||||
optional(user.classId), user.active === false ? 0 : 1, user.mustChangePassword ? 1 : 0, user.displayName, user.createdAt
|
||||
),
|
||||
operation(
|
||||
`INSERT INTO candidate_profiles (
|
||||
id, user_id, name, gender, id_number, phone, email, school, grade, school_id, class_id, address,
|
||||
emergency_contact, emergency_phone, status, review_note, reviewed_at, reviewer_id, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
emergency_contact, emergency_phone, native_place, birth_date, ethnicity, postal_code, guardian_name,
|
||||
guardian_phone, profile_completed, status, review_note, reviewed_at, reviewer_id, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
profile.id, profile.userId, profile.name, optional(profile.gender), profile.idNumber, profile.phone,
|
||||
optional(profile.email), optional(profile.school), optional(profile.grade), optional(profile.schoolId),
|
||||
optional(profile.classId), optional(profile.address),
|
||||
optional(profile.emergencyContact), optional(profile.emergencyPhone), profile.status, optional(profile.reviewNote),
|
||||
optional(profile.emergencyContact), optional(profile.emergencyPhone), optional(profile.nativePlace), optional(profile.birthDate),
|
||||
optional(profile.ethnicity), optional(profile.postalCode), optional(profile.guardianName), optional(profile.guardianPhone),
|
||||
profile.profileCompleted ? 1 : 0, profile.status, optional(profile.reviewNote),
|
||||
optional(profile.reviewedAt), optional(profile.reviewerId), profile.updatedAt
|
||||
)
|
||||
];
|
||||
if (instance && action) operations.push(...workflowCreateOperations(instance, action));
|
||||
if (log) operations.push(auditOperation(log));
|
||||
await transaction(operations);
|
||||
},
|
||||
async updateCandidateProfile(profile, displayName, instance, action) {
|
||||
@@ -1340,12 +1380,14 @@ function createRepository({ client, location, read, transaction, close }) {
|
||||
`UPDATE candidate_profiles SET
|
||||
name = ?, gender = ?, id_number = ?, phone = ?, email = ?, school = ?, grade = ?, address = ?,
|
||||
school_id = ?, class_id = ?, emergency_contact = ?, emergency_phone = ?, status = ?, review_note = ?,
|
||||
reviewed_at = ?, reviewer_id = ?, updated_at = ?
|
||||
native_place = ?, birth_date = ?, ethnicity = ?, postal_code = ?, guardian_name = ?, guardian_phone = ?,
|
||||
profile_completed = ?, reviewed_at = ?, reviewer_id = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
profile.name, optional(profile.gender), profile.idNumber, profile.phone, optional(profile.email),
|
||||
optional(profile.school), optional(profile.grade), optional(profile.address), optional(profile.schoolId),
|
||||
optional(profile.classId), optional(profile.emergencyContact), optional(profile.emergencyPhone), profile.status,
|
||||
optional(profile.reviewNote), optional(profile.reviewedAt),
|
||||
optional(profile.reviewNote), optional(profile.nativePlace), optional(profile.birthDate), optional(profile.ethnicity),
|
||||
optional(profile.postalCode), optional(profile.guardianName), optional(profile.guardianPhone), profile.profileCompleted ? 1 : 0, optional(profile.reviewedAt),
|
||||
optional(profile.reviewerId), profile.updatedAt, profile.id
|
||||
),
|
||||
operation('UPDATE users SET display_name = ? WHERE id = ?', displayName, profile.userId)
|
||||
@@ -1353,6 +1395,20 @@ function createRepository({ client, location, read, transaction, close }) {
|
||||
if (instance && action) operations.push(...workflowCreateOperations(instance, action));
|
||||
await transaction(operations);
|
||||
},
|
||||
async changePassword(user, log) {
|
||||
const operations = [operation(
|
||||
'UPDATE users SET password_hash = ?, must_change_password = ? WHERE id = ?',
|
||||
user.passwordHash, user.mustChangePassword ? 1 : 0, user.id
|
||||
)];
|
||||
if (log) operations.push(auditOperation(log));
|
||||
await transaction(operations);
|
||||
},
|
||||
async updateRegistrationSetting(enabled, log) {
|
||||
await transaction([
|
||||
operation('UPDATE schema_metadata SET self_registration_enabled = ? WHERE id = 1', enabled ? 1 : 0),
|
||||
auditOperation(log)
|
||||
]);
|
||||
},
|
||||
async createRegistration(registration, instance, action) {
|
||||
const operations = [operation(
|
||||
`INSERT INTO registrations (
|
||||
@@ -1595,12 +1651,16 @@ function createRepository({ client, location, read, transaction, close }) {
|
||||
auditOperation(log)
|
||||
]);
|
||||
},
|
||||
async assignRegistrationNumbers(registrations, log) {
|
||||
const operations = registrations.map(registration => operation(
|
||||
async assignCandidateNumbers(users, registrations, log) {
|
||||
const operations = users.map(user => operation(
|
||||
'UPDATE users SET candidate_number = ? WHERE id = ? AND (candidate_number IS NULL OR candidate_number = \'\')',
|
||||
user.candidateNumber, user.id
|
||||
));
|
||||
operations.push(...registrations.map(registration => operation(
|
||||
`UPDATE registrations SET registration_number = ?, number_rule_id = ?
|
||||
WHERE id = ? AND (registration_number IS NULL OR registration_number = '')`,
|
||||
registration.registrationNumber, optional(registration.numberRuleId), registration.id
|
||||
));
|
||||
)));
|
||||
operations.push(auditOperation(log));
|
||||
await transaction(operations);
|
||||
},
|
||||
@@ -1706,9 +1766,14 @@ async function createSqliteStore({ path, seed }) {
|
||||
}
|
||||
};
|
||||
ensureColumns('users', [
|
||||
['admin_level', 'TEXT'], ['school_id', 'TEXT'], ['class_id', 'TEXT'], ['active', 'INTEGER NOT NULL DEFAULT 1']
|
||||
['admin_level', 'TEXT'], ['school_id', 'TEXT'], ['class_id', 'TEXT'], ['active', 'INTEGER NOT NULL DEFAULT 1'],
|
||||
['candidate_number', 'TEXT'], ['must_change_password', 'INTEGER NOT NULL DEFAULT 0']
|
||||
]);
|
||||
ensureColumns('schema_metadata', [['self_registration_enabled', 'INTEGER NOT NULL DEFAULT 0']]);
|
||||
ensureColumns('candidate_profiles', [
|
||||
['school_id', 'TEXT'], ['class_id', 'TEXT'], ['native_place', 'TEXT'], ['birth_date', 'TEXT'], ['ethnicity', 'TEXT'],
|
||||
['postal_code', 'TEXT'], ['guardian_name', 'TEXT'], ['guardian_phone', 'TEXT'], ['profile_completed', 'INTEGER NOT NULL DEFAULT 0']
|
||||
]);
|
||||
ensureColumns('candidate_profiles', [['school_id', 'TEXT'], ['class_id', 'TEXT']]);
|
||||
ensureColumns('registrations', [['registration_number', 'TEXT'], ['number_rule_id', 'TEXT']]);
|
||||
ensureColumns('test_centers', [
|
||||
['code', 'TEXT'], ['manager_name', 'TEXT'], ['manager_phone', 'TEXT'], ['emergency_phone', 'TEXT'],
|
||||
@@ -1738,7 +1803,37 @@ async function createSqliteStore({ path, seed }) {
|
||||
`);
|
||||
}
|
||||
}
|
||||
if (tableExists('registrations')) {
|
||||
const registrationsSql = connection.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registrations'").get()?.sql || '';
|
||||
if (/registration_number\s+TEXT\s+UNIQUE/i.test(registrationsSql)) {
|
||||
connection.exec(`
|
||||
PRAGMA foreign_keys = OFF;
|
||||
BEGIN IMMEDIATE;
|
||||
CREATE TABLE registrations_v4 (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
exam_id TEXT NOT NULL REFERENCES exams(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
|
||||
payment_status TEXT NOT NULL CHECK (payment_status IN ('unpaid', 'paid', 'refunded')),
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_at TEXT,
|
||||
review_note TEXT,
|
||||
registration_number TEXT,
|
||||
number_rule_id TEXT,
|
||||
UNIQUE (user_id, exam_id)
|
||||
) STRICT;
|
||||
INSERT INTO registrations_v4 (
|
||||
id, user_id, exam_id, status, payment_status, created_at, reviewed_at, review_note, registration_number, number_rule_id
|
||||
) SELECT id, user_id, exam_id, status, payment_status, created_at, reviewed_at, review_note, registration_number, number_rule_id FROM registrations;
|
||||
DROP TABLE registrations;
|
||||
ALTER TABLE registrations_v4 RENAME TO registrations;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys = ON;
|
||||
`);
|
||||
}
|
||||
}
|
||||
connection.exec(sqliteSchema);
|
||||
connection.exec('CREATE UNIQUE INDEX IF NOT EXISTS uq_users_candidate_number ON users(candidate_number)');
|
||||
|
||||
const existingSystem = connection.prepare('SELECT * FROM schema_metadata WHERE id = 1').get();
|
||||
if (existingSystem && Number(existingSystem.app_version || 1) < 2) {
|
||||
@@ -1833,14 +1928,44 @@ async function createSqliteStore({ path, seed }) {
|
||||
}
|
||||
}
|
||||
|
||||
if (existingSystem && Number(existingSystem.app_version || 1) < 4) {
|
||||
const extension = seed();
|
||||
connection.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
for (const user of extension.users.filter(item => item.role === 'candidate')) connection.prepare(
|
||||
`UPDATE users SET candidate_number = COALESCE(NULLIF(candidate_number, ''), ?),
|
||||
must_change_password = COALESCE(must_change_password, ?) WHERE id = ?`
|
||||
).run(optional(user.candidateNumber), user.mustChangePassword ? 1 : 0, user.id);
|
||||
connection.prepare(`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),
|
||||
'CAND-' || substr(id, -10)
|
||||
) WHERE role = 'candidate' AND (candidate_number IS NULL OR candidate_number = '')`).run();
|
||||
for (const profile of extension.candidateProfiles) connection.prepare(
|
||||
`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 = ?`
|
||||
).run(optional(profile.nativePlace), optional(profile.birthDate), optional(profile.ethnicity), optional(profile.postalCode),
|
||||
optional(profile.guardianName), optional(profile.guardianPhone), profile.profileCompleted ? 1 : 0, profile.id);
|
||||
connection.prepare(`UPDATE registrations SET registration_number = (
|
||||
SELECT candidate_number FROM users WHERE users.id = registrations.user_id
|
||||
) WHERE registration_number IS NULL OR registration_number = ''`).run();
|
||||
connection.prepare('UPDATE schema_metadata SET schema_version = 4, app_version = 4 WHERE id = 1').run();
|
||||
connection.exec('COMMIT');
|
||||
} catch (error) {
|
||||
connection.exec('ROLLBACK');
|
||||
connection.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.prepare('SELECT id FROM schema_metadata WHERE id = 1').get()) {
|
||||
const initialState = seed();
|
||||
connection.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
connection.prepare(`
|
||||
INSERT INTO schema_metadata (id, schema_version, app_version, created_at)
|
||||
VALUES (1, 3, ?, ?)
|
||||
`).run(Number(initialState.meta?.version || 1), initialState.meta?.createdAt || new Date().toISOString());
|
||||
INSERT INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at)
|
||||
VALUES (1, 4, ?, ?, ?)
|
||||
`).run(Number(initialState.meta?.version || 1), initialState.settings?.selfRegistrationEnabled ? 1 : 0, initialState.meta?.createdAt || new Date().toISOString());
|
||||
for (const item of buildSeedOperations(initialState)) connection.prepare(item.sql).run(...item.params);
|
||||
connection.exec('COMMIT');
|
||||
} catch (error) {
|
||||
@@ -1899,8 +2024,18 @@ async function createMysqlStore({ seed }) {
|
||||
'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 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 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 test_centers ADD COLUMN IF NOT EXISTS code VARCHAR(40) NULL',
|
||||
@@ -1914,6 +2049,8 @@ async function createMysqlStore({ seed }) {
|
||||
"ALTER TABLE workflow_definitions MODIFY COLUMN business_type ENUM('profile_change', 'registration_review', 'center_change') NOT NULL"
|
||||
];
|
||||
for (const statement of mysqlColumnMigrations) 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 FROM schema_metadata WHERE id = 1');
|
||||
@@ -2023,6 +2160,39 @@ async function createMysqlStore({ seed }) {
|
||||
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 (!existing.length) {
|
||||
const initialState = seed();
|
||||
@@ -2030,9 +2200,9 @@ async function createMysqlStore({ seed }) {
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const [insert] = await connection.execute(`
|
||||
INSERT IGNORE INTO schema_metadata (id, schema_version, app_version, created_at)
|
||||
VALUES (1, 3, ?, ?)
|
||||
`, [Number(initialState.meta?.version || 1), initialState.meta?.createdAt || new Date().toISOString()]);
|
||||
INSERT IGNORE INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at)
|
||||
VALUES (1, 4, ?, ?, ?)
|
||||
`, [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);
|
||||
}
|
||||
@@ -2049,6 +2219,10 @@ async function createMysqlStore({ seed }) {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user