import mysql from 'mysql'; import semver from 'semver'; import { fatal, getVersion } from './utils'; import { NotFoundError } from './errors'; const DATABASE = 'chronos'; export default class Database { constructor(options) { this.connection = mysql.createConnection(options); this.connection.connect(); this.checkAndMigrate(); } async getSchools() { return this.query(` SELECT id, name, domain FROM school `); } async getSchoolWithAuth(id) { return this.query(` SELECT * FROM auth RIGHT JOIN school ON auth.school = school.id WHERE school.${isNaN(parseInt(id, 10)) ? 'domain' : 'id'} = ? `, [id], { required: true, }) .then(results => ({ id: results[0].id, name: results[0].name, domain: results[0].domain, auth: results[0].type ? results.map(r => Object.assign(r, { school: undefined, name: undefined, domain: undefined, })) : [], })) .catch(err => Promise.reject(err.withNoun('School'))); } // eslint-disable-next-line class-methods-use-this async getUsers(school) { return this.query(` SELECT id, name FROM user WHERE user.school = ? `, [school]); } async getUser(id) { return this.query(` SELECT * FROM user WHERE user.id = ? `, [id], { single: true, }) .catch(err => Promise.reject(err.withNoun('User'))); } async getUserWithSchool(school, id) { return this.query(` SELECT * FROM user WHERE user.school = ? AND user.id = ? `, [school, id], { single: true, }) .catch(err => Promise.reject(err.withNoun('User'))); } async getUserByEmail(email) { // assumes unique email return this.query(` SELECT * FROM user WHERE user.email = ? `, [email], { single: true, }) .catch(err => Promise.reject(err.withNoun('User'))); } query(query, values, options = {}) { console.log('QUERY:', query.replace(/[\n\t]+/g, ' ').replace(/^ /g, '').replace(/ $/g, '')); return new Promise((resolve, reject) => { this.connection.query(query, values, (err, results, fields) => { if (err) { reject(err); } else if ((options.required || options.single) && results.length < 1) { reject(new NotFoundError()); } if (options.single) { resolve(results[0], fields); } else { resolve(results, fields); } }); }); } async checkAndMigrate() { return this.query(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`) .then(() => this.query(`USE ${DATABASE}`)) .then(() => this.query('SELECT * FROM options WHERE option = ?', ['VERSION']) .then(result => this.migrate(result[0] ? result[0].value : null)) .catch(() => this.migrate())) .catch(err => fatal(err)); } async migrate(oldVersion) { if (semver.satisfies(oldVersion, '~1')) { // database is up-to-date return true; } else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration return true; // database needs to be updated // return this.query(`DROP DATABASE ${DATABASE}`) // .then(() => this.checkAndMigrate()); } // database does not exist // create tables const tables = [ `CREATE TABLE options ( option VARCHAR(12) NOT NULL, value VARCHAR(64) NOT NULL, PRIMARY KEY (option) )`, `CREATE TABLE school ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, domain VARCHAR(32), PRIMARY KEY (id) )`, `CREATE TABLE auth ( school INT NOT NULL, id INT NOT NULL, type CHAR(3) NOT NULL, oid_meta VARCHAR(128), oid_cid VARCHAR(64), oid_csecret VARCHAR(64), PRIMARY KEY (school, id), FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE holiday ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, start DATETIME NOT NULL, end DATETIME NOT NULL, PRIMARY KEY (id) )`, `CREATE TABLE observes ( school INT NOT NULL, holiday INT NOT NULL, FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (holiday) REFERENCES holiday(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE user ( school INT NOT NULL, id INT AUTO_INCREMENT NOT NULL, name VARCHAR(64), email VARCHAR(64), oid_id VARCHAR(64), pwd_hash VARCHAR(64), role CHAR(3), PRIMARY KEY (id), FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE group_ ( id INT AUTO_INCREMENT NOT NULL, name VARCHAR(64) NOT NULL, type CHAR(3), PRIMARY KEY (id) )`, `CREATE TABLE group_mentor ( id INT NOT NULL, level TINYINT NOT NULL, year YEAR(4) NOT NULL, PRIMARY KEY (id), FOREIGN KEY (id) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE member ( user INT NOT NULL, group_ INT NOT NULL, FOREIGN KEY (user) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE event_once ( group_ INT NOT NULL, id INT AUTO_INCREMENT NOT NULL, name VARCHAR(64) NOT NULL, start DATETIME NOT NULL, end DATETIME NOT NULL, PRIMARY KEY (id), FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE attachment ( event_once INT NOT NULL, id INT NOT NULL, PRIMARY KEY (event_once, id), FOREIGN KEY (event_once) REFERENCES event_once(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE event_weekly ( group_ INT NOT NULL, id INT AUTO_INCREMENT NOT NULL, name VARCHAR(64) NOT NULL, day TINYINT NOT NULL, starttime TIME NOT NULL, endtime TIME NOT NULL, PRIMARY KEY (id), FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE )`, `CREATE TABLE ignored ( user INT NOT NULL, event_weekly INT NOT NULL, FOREIGN KEY (user) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (event_weekly) REFERENCES event_weekly(id) ON DELETE CASCADE ON UPDATE CASCADE )`, ]; for (let i = 0; i < tables.length; i += 1) { // eslint-disable-next-line no-await-in-loop await this.query(tables[i]); } // set version number await this.query(` INSERT INTO options (option, value) VALUES ('version', '${getVersion()}') `); // TODO: Build admin interface to add schools and school owner // add first school const firstSchool = await this.query(` INSERT INTO school (name, domain) VALUES (?, ?) `, ['NUS High School', 'nushigh.edu.sg']); await this.query(` INSERT INTO user (school, name, email, pwd_hash, role) VALUES (?, ?, ?, ?, ?) `, [firstSchool.insertId, 'Ambrose Chua', 'h1310031@nushigh.edu.sg', '', 'OWN']); // eslint-disable-next-line global-require const fs = require('fs'); const tmpsettings = JSON.parse(fs.readFileSync('oid_settings.json', 'utf8')); await this.query(` INSERT INTO auth (school, type, oid_meta, oid_cid, oid_csecret) VALUES (?, ?, ?, ?, ?) `, [firstSchool.insertId, 'OID', tmpsettings.oid_meta, tmpsettings.oid_cid, tmpsettings.oid_csecret]); return true; } }