378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
import mysql from 'mysql';
|
|
import semver from 'semver';
|
|
|
|
import { fatal, getVersion, stripTimezone } from './utils';
|
|
import { NotFoundError, attachNoun } 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(attachNoun('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(school, id) {
|
|
return this.query(`
|
|
SELECT *
|
|
FROM user
|
|
WHERE user.school = ?
|
|
AND user.id = ?
|
|
`, [school, id], {
|
|
single: true,
|
|
})
|
|
.catch(attachNoun('User'));
|
|
}
|
|
async getUserByEmail(school, email) {
|
|
// assumes unique email
|
|
return this.query(`
|
|
SELECT *
|
|
FROM user
|
|
WHERE user.school = ?
|
|
AND user.email = ?
|
|
`, [school, email], {
|
|
single: true,
|
|
})
|
|
.catch(attachNoun('User'));
|
|
}
|
|
async getUserGroups(school, id) {
|
|
return this.query(`
|
|
SELECT group_.*
|
|
FROM member, group_
|
|
WHERE member.group_ = group_.id
|
|
AND member.user = ?
|
|
`, [id])
|
|
.catch(attachNoun('Group'));
|
|
}
|
|
|
|
async getGroups(school) {
|
|
return this.query(`
|
|
SELECT DISTINCT group_.*
|
|
FROM user, member, group_
|
|
WHERE member.group_ = group_.id
|
|
AND member.user = user.id
|
|
AND user.school = ?
|
|
`, [school]);
|
|
}
|
|
async createGroup(school, data) {
|
|
console.log(data);
|
|
const group = await this.query(`
|
|
INSERT INTO group_ (name, type)
|
|
VALUES (?, ?)
|
|
`, [data.name, data.type]);
|
|
if (data.type === 'MEN') {
|
|
await this.query(`
|
|
INSERT INTO group_mentor (id, level, year)
|
|
VALUES (?, ?, ?)
|
|
`, [group.insertId, data.mentor_level, data.mentor_year]);
|
|
}
|
|
const insertMember = u => this.query(`
|
|
INSERT INTO member (user, group_)
|
|
VALUES (?, ?)
|
|
`, [u, group.insertId]);
|
|
return Promise.all(data.members.map(insertMember));
|
|
}
|
|
async getGroup(school, id) {
|
|
const getGroup = this.query(`
|
|
SELECT *
|
|
FROM group_
|
|
WHERE group_.id = ?
|
|
`, [id], {
|
|
required: true,
|
|
single: true,
|
|
}).catch(attachNoun('Group'));
|
|
const getMembers = this.query(`
|
|
SELECT user.*
|
|
FROM member, user
|
|
WHERE member.user = user.id
|
|
AND member.group_ = ?
|
|
`, [id]);
|
|
const getEventsOnce = this.query(`
|
|
SELECT *
|
|
FROM event_once
|
|
WHERE event_once.group_ = ?
|
|
`, [id]);
|
|
const getEventsWeekly = this.query(`
|
|
SELECT *
|
|
FROM event_weekly
|
|
WHERE event_weekly.group_ = ?
|
|
`, [id]);
|
|
return Promise.all([getGroup, getMembers, getEventsOnce, getEventsWeekly])
|
|
.then(results => Object.assign({}, results[0], {
|
|
members: results[1],
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
eventsOnce: results[2].map(e => Object.assign({ group: e.group_ }, e)),
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
eventsWeekly: results[3].map(e => Object.assign({ group: e.group_ }, e)),
|
|
}));
|
|
}
|
|
|
|
async createEventWeekly(school, group, data) {
|
|
console.log(data.starttime);
|
|
return this.query(`
|
|
INSERT INTO event_weekly (group_, name, day, starttime, endtime)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`, [group, data.name, data.day, stripTimezone(data.starttime), stripTimezone(data.endtime)]);
|
|
}
|
|
|
|
async createEventOnce(school, group, data) {
|
|
console.log(data.starttime);
|
|
return this.query(`
|
|
INSERT INTO event_once (group_, name, start, end)
|
|
VALUES (?, ?, ?, ?)
|
|
`, [group, data.name, stripTimezone(data.start), stripTimezone(data.end)]);
|
|
}
|
|
async getEventOnce(school, group, id) {
|
|
return this.query(`
|
|
SELECT *
|
|
FROM event_once
|
|
WHERE event_once.group_ = ?
|
|
AND event_once.id = ?
|
|
`, [group, id])
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
.then(e => Object.assign({ group: e.group_ }, e));
|
|
}
|
|
|
|
async getUserEventsBetween(school, user, start, end) {
|
|
// oh shit
|
|
const getEventsOnce = this.query(`
|
|
SELECT event_once.*
|
|
FROM member, event_once
|
|
WHERE member.group_ = event_once.group_
|
|
AND member.user = ?
|
|
|
|
AND (
|
|
(event_once.start >= ? AND event_once.start <= ?)
|
|
OR
|
|
(event_once.end >= ? AND event_once.end <= ?)
|
|
)
|
|
`, [user, stripTimezone(start), stripTimezone(end), stripTimezone(start), stripTimezone(end)]);
|
|
const getEventsWeekly = this.query(`
|
|
SELECT event_weekly.*
|
|
FROM member, event_weekly
|
|
WHERE member.group_ = event_weekly.group_
|
|
AND member.user = ?
|
|
`, [user]);
|
|
return Promise.all([getEventsOnce, getEventsWeekly])
|
|
.then(results => ({
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
eventsOnce: results[0].map(e => Object.assign({ group: e.group_ }, e)),
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
eventsWeekly: results[1].map(e => Object.assign({ group: e.group_ }, e)),
|
|
}));
|
|
}
|
|
|
|
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),
|
|
email_regex VARCHAR(128),
|
|
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),
|
|
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,
|
|
role CHAR(3),
|
|
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,
|
|
type CHAR(3) NOT NULL,
|
|
text_oid VARCHAR(1024) 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, role)
|
|
VALUES (?, ?, ?, ?)
|
|
`, [firstSchool.insertId, 'Admin', 'admin@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]);
|
|
|
|
if (process.env.GENERATE_DB_ONLY === 'true') {
|
|
process.exit(0);
|
|
}
|
|
return true;
|
|
}
|
|
}
|