267 lines
10 KiB
JavaScript
Executable File
267 lines
10 KiB
JavaScript
Executable File
// Database Schema Initialization
|
||
// Auto-creates all required tables on application startup
|
||
|
||
import pool from './db.js';
|
||
|
||
const createTables = async () => {
|
||
const connection = await pool.getConnection();
|
||
|
||
try {
|
||
console.log('🔧 Initializing Database Schema...');
|
||
|
||
// 1. Users Table
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id VARCHAR(36) PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
nis VARCHAR(50) UNIQUE,
|
||
class_name VARCHAR(100),
|
||
shift ENUM('PAGI', 'SIANG') DEFAULT 'PAGI',
|
||
role ENUM('ADMIN', 'STUDENT', 'TEACHER') DEFAULT 'STUDENT',
|
||
registered_face LONGTEXT,
|
||
face_descriptor LONGTEXT,
|
||
created_at BIGINT,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
INDEX idx_nis (nis),
|
||
INDEX idx_class (class_name),
|
||
INDEX idx_role (role)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
|
||
// 1.1 Add face_descriptor column if not exists (Migration)
|
||
try {
|
||
await connection.query(`
|
||
SELECT count(*) FROM information_schema.COLUMNS
|
||
WHERE (TABLE_SCHEMA = '${process.env.DB_NAME}' OR TABLE_SCHEMA = DATABASE())
|
||
AND TABLE_NAME = 'users'
|
||
AND COLUMN_NAME = 'face_descriptor'
|
||
`).then(async ([rows]) => {
|
||
// @ts-ignore
|
||
if (rows[0]['count(*)'] === 0 && rows[0]['COUNT(*)'] === 0) {
|
||
console.log(' ⚠️ Migrating table "users": adding face_descriptor column...');
|
||
await connection.query('ALTER TABLE users ADD COLUMN face_descriptor LONGTEXT AFTER registered_face');
|
||
console.log(' ✓ Migration successful');
|
||
}
|
||
});
|
||
} catch (migErr) {
|
||
// Ignore error if column check fails, it might be already created by CREATE TABLE
|
||
console.log(' ℹ️ Table check skipped or passed');
|
||
}
|
||
|
||
console.log(' ✓ Table "users" ready');
|
||
|
||
// 2. Attendance Table
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS attendance (
|
||
id VARCHAR(36) PRIMARY KEY,
|
||
user_id VARCHAR(36),
|
||
user_name VARCHAR(255),
|
||
nis VARCHAR(50),
|
||
class_name VARCHAR(100),
|
||
timestamp BIGINT,
|
||
date_str VARCHAR(20),
|
||
time_str VARCHAR(10),
|
||
lat DECIMAL(10, 7),
|
||
lng DECIMAL(10, 7),
|
||
distance DECIMAL(10, 2),
|
||
photo_evidence LONGTEXT,
|
||
status ENUM('PRESENT', 'LATE', 'REGISTRATION', 'ALFA') DEFAULT 'PRESENT',
|
||
ai_verification TEXT,
|
||
parent_phone VARCHAR(20),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_date (date_str),
|
||
INDEX idx_user (user_id),
|
||
INDEX idx_nis (nis),
|
||
INDEX idx_status (status)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
console.log(' ✓ Table "attendance" ready');
|
||
|
||
// 3. Registrations Table (Face Registration History)
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS registrations (
|
||
id VARCHAR(36) PRIMARY KEY,
|
||
user_id VARCHAR(36),
|
||
nis VARCHAR(50),
|
||
user_name VARCHAR(255),
|
||
class_name VARCHAR(100),
|
||
photo_url LONGTEXT,
|
||
timestamp BIGINT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_nis (nis),
|
||
INDEX idx_user (user_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
console.log(' ✓ Table "registrations" ready');
|
||
|
||
// 4. Settings Table (Key-Value Store)
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
setting_key VARCHAR(100) UNIQUE NOT NULL,
|
||
setting_value TEXT,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
INDEX idx_key (setting_key)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
console.log(' ✓ Table "settings" ready');
|
||
|
||
// 5. Staff Users Table (Admin, Guru, Guru BK) - Dynamic User Management
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS staff_users (
|
||
id VARCHAR(36) PRIMARY KEY,
|
||
username VARCHAR(100) UNIQUE NOT NULL,
|
||
password VARCHAR(255) NOT NULL,
|
||
name VARCHAR(255) NOT NULL,
|
||
role ENUM('ADMIN', 'TEACHER', 'GURU_BK') NOT NULL,
|
||
phone VARCHAR(20),
|
||
assigned_classes TEXT,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
INDEX idx_username (username),
|
||
INDEX idx_role (role)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
console.log(' ✓ Table "staff_users" ready');
|
||
|
||
// 5.1 Add phone column to staff_users if not exists (Migration)
|
||
try {
|
||
await connection.query(`ALTER TABLE staff_users ADD COLUMN phone VARCHAR(20) AFTER role`);
|
||
console.log(' ✓ Added phone column to staff_users');
|
||
} catch (migErr) {
|
||
// Column already exists
|
||
}
|
||
|
||
// 5.2 Add assigned_classes column to staff_users if not exists (Migration)
|
||
try {
|
||
await connection.query(`ALTER TABLE staff_users ADD COLUMN assigned_classes TEXT AFTER phone`);
|
||
console.log(' ✓ Added assigned_classes column to staff_users');
|
||
} catch (migErr) {
|
||
// Column already exists
|
||
}
|
||
|
||
// 5.2 Add SAKIT, IZIN, and DISPEN to attendance status (Migration)
|
||
try {
|
||
await connection.query(`
|
||
ALTER TABLE attendance
|
||
MODIFY COLUMN status ENUM('PRESENT', 'LATE', 'REGISTRATION', 'ALFA', 'SAKIT', 'IZIN', 'DISPEN') DEFAULT 'PRESENT'
|
||
`);
|
||
console.log(' ✓ Attendance status updated (added SAKIT, IZIN, DISPEN)');
|
||
} catch (migErr) {
|
||
// Ignore if already updated
|
||
}
|
||
|
||
// 6. Leave Requests Table (Pengajuan Izin/Sakit/Dispensasi oleh Siswa)
|
||
await connection.query(`
|
||
CREATE TABLE IF NOT EXISTS leave_requests (
|
||
id VARCHAR(36) PRIMARY KEY,
|
||
student_id VARCHAR(36) NOT NULL,
|
||
student_name VARCHAR(255) NOT NULL,
|
||
student_nis VARCHAR(50),
|
||
student_class VARCHAR(100),
|
||
request_type ENUM('SAKIT', 'IZIN', 'DISPEN') NOT NULL,
|
||
request_date VARCHAR(20) NOT NULL,
|
||
reason TEXT NOT NULL,
|
||
photo_evidence LONGTEXT,
|
||
status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
|
||
reviewed_by VARCHAR(36),
|
||
reviewed_by_name VARCHAR(255),
|
||
reviewed_at TIMESTAMP NULL,
|
||
rejection_reason TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_student (student_id),
|
||
INDEX idx_status (status),
|
||
INDEX idx_date (request_date)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
console.log(' ✓ Table "leave_requests" ready');
|
||
|
||
// 6.1 Add DISPEN to leave_requests request_type (Migration)
|
||
try {
|
||
await connection.query(`
|
||
ALTER TABLE leave_requests
|
||
MODIFY COLUMN request_type ENUM('SAKIT', 'IZIN', 'DISPEN') NOT NULL
|
||
`);
|
||
console.log(' ✓ Leave requests type updated (added DISPEN)');
|
||
} catch (migErr) {
|
||
// Ignore if already updated
|
||
}
|
||
|
||
// 5. Insert Default Settings if empty
|
||
const [settingsRows] = await connection.query('SELECT COUNT(*) as count FROM settings');
|
||
if (settingsRows[0].count === 0) {
|
||
const defaultSettings = [
|
||
['NAMA_SEKOLAH', 'SMA Negeri 1 Abiansemal'],
|
||
['LATITUDE', '-8.5107893'],
|
||
['LONGITUDE', '115.2142912'],
|
||
['RADIUS_METER', '100'],
|
||
['JAM_MASUK_PAGI', '07:00'],
|
||
['JAM_PULANG_PAGI', '12:00'],
|
||
['JAM_MASUK_SIANG', '12:30'],
|
||
['JAM_PULANG_SIANG', '16:00'],
|
||
['HARI_AKTIF', '1,2,3,4,5,6'],
|
||
['DAFTAR_KELAS', 'X-1,X-2,XI-1,XI-2,XII-1,XII-2'],
|
||
['SEMESTER', 'Ganjil'],
|
||
['TAHUN_AJARAN', '2023/2024'],
|
||
['AMBANG_WAJAH', '0.45'],
|
||
['AUTO_REKAP_ALFA_TIME', '19:00']
|
||
];
|
||
|
||
for (const [key, value] of defaultSettings) {
|
||
await connection.query(
|
||
'INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?',
|
||
[key, value, value]
|
||
);
|
||
}
|
||
console.log(' ✓ Default settings inserted');
|
||
}
|
||
|
||
// 5.1 Ensure FONNTE_TOKEN exists
|
||
try {
|
||
await connection.query(
|
||
"INSERT INTO settings (setting_key, setting_value) VALUES ('FONNTE_TOKEN', '') ON DUPLICATE KEY UPDATE setting_key = setting_key"
|
||
);
|
||
} catch (err) {
|
||
// Ignore
|
||
}
|
||
|
||
// 6. AUTO-MIGRATION: Create optimized indexes for daily attendance queries
|
||
console.log(' 📊 Optimizing database indexes...');
|
||
const optimizedIndexes = [
|
||
// Composite indexes for filtered daily queries
|
||
{ name: 'idx_attendance_date_class', table: 'attendance', columns: 'date_str, class_name' },
|
||
{ name: 'idx_attendance_date_status', table: 'attendance', columns: 'date_str, status' },
|
||
{ name: 'idx_attendance_date_class_status', table: 'attendance', columns: 'date_str, class_name, status' },
|
||
// Index for user name search (LIKE queries)
|
||
{ name: 'idx_attendance_username', table: 'attendance', columns: 'user_name' },
|
||
// Index for pagination ordering
|
||
{ name: 'idx_attendance_classname_username', table: 'attendance', columns: 'class_name, user_name' },
|
||
];
|
||
|
||
for (const idx of optimizedIndexes) {
|
||
try {
|
||
await connection.query(`CREATE INDEX ${idx.name} ON ${idx.table}(${idx.columns})`);
|
||
console.log(` ✓ Index "${idx.name}" created`);
|
||
} catch (indexErr) {
|
||
// Index already exists - this is fine
|
||
if (indexErr.code === 'ER_DUP_KEYNAME') {
|
||
// Silent - already exists
|
||
} else {
|
||
console.log(` ℹ️ Index "${idx.name}" skipped: ${indexErr.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('✅ Database Schema Initialized Successfully!');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Schema Initialization Error:', error.message);
|
||
throw error;
|
||
} finally {
|
||
connection.release();
|
||
}
|
||
};
|
||
|
||
export default createTables;
|