diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a63e357 --- /dev/null +++ b/.eslintrc @@ -0,0 +1 @@ +{"rules": {"indent": ["error", "tab"], "no-tabs": "off", "react/jsx-indent": ["error", "tab"]}, "extends": "airbnb", "env": {"browser": true, "node": true}} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..db09d1c --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,81 @@ +module.exports = function (grunt) { + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + webpack: { + app: { + entry: './app/index.jsx', + output: { + filename: 'bundle.js', + path: './dist/app/' + }, + module: { + loaders: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + sourceMap: true, + presets: ['env', 'react'] + } + } + ] + } + } + }, + copy: { + app: { + files: [ + { + expand: true, + cwd: './app/', + src: ['index.html'], + dest: './dist/app/' + }, + { + expand: true, + cwd: './app/', + src: ['assets/**'], + dest: './dist/app/assets/' + } + ] + } + }, + babel: { + options: { + sourceMap: true, + presets: [ + ['env', { + targets: { + node: 'current' + } + }] + ] + }, + server: { + files: [ + { + expand: true, + cwd: './server/', + src: ['*.js'], + dest: './dist/server/' + } + ] + } + }, + eslint: { + app: ['./app/*.js*'], + server: ['./server/*.js'] + } + }); + + grunt.loadNpmTasks('grunt-webpack'); + grunt.loadNpmTasks('grunt-babel'); + grunt.loadNpmTasks('grunt-eslint'); + grunt.loadNpmTasks('grunt-contrib-copy'); + + grunt.registerTask('all', ['eslint', 'webpack', 'copy', 'babel']); + grunt.registerTask('default', ['all']); + +}; diff --git a/app/assets/core.css b/app/assets/core.css new file mode 100644 index 0000000..e69de29 diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..0a00260 --- /dev/null +++ b/app/index.html @@ -0,0 +1,10 @@ + + + Chronos + + +
+ +
+ + diff --git a/app/index.jsx b/app/index.jsx new file mode 100644 index 0000000..38ce17c --- /dev/null +++ b/app/index.jsx @@ -0,0 +1,7 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +ReactDOM.render( +

Hello, world!

, + document.getElementById('root'), +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd0abb2 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "Chronos", + "version": "0.1.0", + "description": "A school event planner and timetable", + "scripts": { + "start": "node dist/server/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ambrosechua/chronos.git" + }, + "keywords": [], + "author": "", + "license": "proprietary", + "bugs": { + "url": "https://github.com/ambrosechua/chronos/issues" + }, + "homepage": "https://github.com/ambrosechua/chronos#readme", + "devDependencies": { + "babel-loader": "^6.3.2", + "babel-preset-env": "^1.1.10", + "babel-preset-react": "^6.23.0", + "eslint": "^3.16.1", + "eslint-config-airbnb": "^14.1.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^4.0.0", + "eslint-plugin-react": "^6.10.0", + "grunt": "^1.0.1", + "grunt-babel": "^6.0.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-eslint": "^19.0.0", + "grunt-webpack": "^2.0.1", + "webpack": "^2.2.1" + }, + "dependencies": { + "express": "^4.14.1", + "mysql": "^2.13.0", + "react": "^15.4.2", + "react-dom": "^15.4.2" + } +} diff --git a/server/api.js b/server/api.js new file mode 100644 index 0000000..74367c7 --- /dev/null +++ b/server/api.js @@ -0,0 +1,13 @@ +import Router from 'express'; + +export default class API { + constructor() { + this.router = Router({ + strict: true, + }); + + this.router.get('/', (req, res) => { + res.end('API v1'); + }); + } +} diff --git a/server/database.js b/server/database.js new file mode 100644 index 0000000..a53807a --- /dev/null +++ b/server/database.js @@ -0,0 +1,156 @@ +import mysql from 'mysql'; +import { fatal, getVersion } from './utils'; + +const DATABASE = 'chronos'; + +export default class Database { + constructor(options) { + this.connection = mysql.createConnection(options); + this.connection.connect(); + + this.checkAndMigrate(); + } + + query(query, values) { + console.log('QUERY:', query); + return new Promise((resolve, reject) => { + this.connection.query(query, values, (err, results, fields) => { + if (err) { + reject(err); + } + resolve(results, fields); + }); + }); + } + + checkAndMigrate() { + this.query(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`, []) + .then(() => this.query(`USE ${DATABASE}`, [])) + .then(() => this.query('SELECT value FROM options WHERE option = "version"', (err, result) => { + if (err || result === undefined || getVersion() !== result[0].value) { + return this.migrate(result).catch(e => fatal(e)); + } + return true; + })) + .catch(err => fatal(err)); + } + + async migrate(oldVersion) { + if (!oldVersion) { + // 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 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], []); + } + + await this.query(` + INSERT INTO options (option, value) + VALUES ('version', '${getVersion()}') + `); + + + return true; + } + // for now + return this.query(`DROP DATABASE ${DATABASE}`, []) + .then(() => this.checkAndMigrate()); + } +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..d9af39b --- /dev/null +++ b/server/index.js @@ -0,0 +1,17 @@ +import path from 'path'; +import express from 'express'; +import Database from './database'; +import API from './api'; + +const app = express(); +const database = new Database({ + host: 'localhost', + user: 'root', + password: '', +}); +const api = new API(database); + +app.use('/', express.static(path.join(__dirname, '..', 'app'))); +app.use('/api/v1', api.router); + +app.listen(8080); diff --git a/server/utils.js b/server/utils.js new file mode 100644 index 0000000..fc6a58c --- /dev/null +++ b/server/utils.js @@ -0,0 +1,13 @@ +const getVersion = () => { + if (process.env.npm_package_version) { + return process.env.npm_package_version; + } + throw new Error('Unable to obtain running version'); +}; + +const fatal = (e) => { + console.error(e); + process.exit(1); +}; + +export { getVersion, fatal };