Files
smanab/backend/schema.js
2026-02-22 14:54:55 +08:00

267 lines
10 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;