const fs = require('fs'); const db = require('db'); const log = require('log'); const bc = require('bcryptjs'); let models = []; function postgresToCamel(postgres) { let ret = ''; for (let i = 0; i < postgres.length; i++) { let c = postgres.charAt(i); if (c === '_') { i++; ret += postgres.charAt(i).toUpperCase(); } else { ret += c; } } return ret; } function camelToPostgres(camelCase) { let ret = ''; for (let i = 0; i < camelCase.length; i++) { let c = camelCase.charAt(i); if (c === c.toUpperCase()) { ret += '_' + c.toLowerCase() } else { ret += c; } } return ret; } function attributeToGetBy(attributeName) { return 'getBy' + attributeName.charAt(0).toUpperCase() + attributeName.substr(1); } module.exports.BaseModel = class { constructor(name) { models.push(this); this.name = name; this.fields = []; } addField(field) { this.fields.push(field); } reinitialize(callback = () => {}) { log.debug('Reinitializing ' + this.name); let query = "DROP TABLE IF EXISTS " + camelToPostgres(this.name) + " CASCADE;"; db.query(query) .then(res => this.createTable(callback)) .catch(err => log.dberror(err)); } createTable(callback) { let query = 'CREATE TABLE ' + camelToPostgres(this.name) + '(\n'; query += this.fields .map(f => '\t' + f.getCreationString()) .join(',\n'); query += ');'; log.debug(query); db.query(query) .then(res => { log.debug("Table " + camelToPostgres(this.name) + " created") callback(); }) .catch(err => log.dberror(err)); } } module.exports.BaseField = class { constructor(name, properties = {}) { this.name = name; this.unique = properties.unique || false; this.notNull = properties.notNull || false; this.default = properties.default; } getCreationString() { return [ camelToPostgres(this.name), this.getPostgresType(), this.getFormattedPropeties(), ].join(' '); } getFormattedPropeties() { let properties = []; if (this.unique) { properties.push('UNIQUE'); } if (this.notNull) { properties.push('NOT NULL'); } return properties.join(' '); } createMutators() { return { get: this.createGetter(), set: this.createSetter(), }; } createGetter() { var self = this; return function() { return this['_' + self.name].value; } } createSetter() { var self = this; return function(e) { this['_' + self.name] = { value: e, changed: true, }; } } } module.exports.SmallIntegerField = class extends module.exports.BaseField { constructor(name, properties = {}) { super(name, properties); } getPostgresType() { return "SMALLINT"; } } module.exports.SerialField = class extends module.exports.BaseField { constructor(name, properties = {}) { super(name, properties); this.unique = true; } getPostgresType() { return "SERIAL"; } } module.exports.TextField = class extends module.exports.BaseField { constructor(name, properties = {}) { super(name, properties); } getPostgresType() { return "TEXT"; } } module.exports.PasswordField = class extends module.exports.BaseField { constructor(name, properties = {}) { super(name, properties); } getPostgresType() { return 'TEXT'; } createSetter() { let self = this; return function(e) { this['_' + self.name] = { value: bc.hashSync(e, 8), changed: true, }; } } } module.exports.PasswordField.testSync = function(password, hash) { return bc.compareSync(password, hash); } module.exports.BoolField = class extends module.exports.BaseField { constructor(name, properties) { super(name, properties); } getPostgresType() { return 'BOOLEAN'; } } module.exports.createClass = function(model) { let ret = function(param) { this._persisted = false; for (let field of model.fields) { this['_' + field.name] = { value: field.default, changed: true, } Object.defineProperty(this, field.name, field.createMutators()); } }; for (let field of model.fields) { if (!field.unique && field.name !== 'activationKey') continue; ret[attributeToGetBy(field.name)] = function(value, callback) { let query = 'SELECT * FROM ' + camelToPostgres(model.name) + ' WHERE ' + camelToPostgres(field.name) + '=$1;'; db .query(query, [value]) .then(res => { // Create the instance let instance = new ret(); instance._persisted = true; instance._setFromDbResult(res); callback(undefined, instance); }) .catch(err => callback(err)); } } ret.prototype._setFromDbResult = function(res) { for (let field of model.fields) { this['_' + field.name] = { value: res.rows[0][camelToPostgres(field.name)], changed: false, }; } } ret.prototype.save = function(callback = () => {}) { let fieldsToSave = []; for (let field of model.fields) { if (this['_' + field.name].changed) { fieldsToSave.push({ field: field, value: this[field.name], }); } } fieldsToSave = fieldsToSave.filter((field) => { return !(field.field instanceof module.exports.SerialField); }); if (!this._persisted) { // INSERT INTO let queryBegin = 'INSERT INTO ' + camelToPostgres(model.name) + '(' let queryEnd = ' VALUES ('; queryBegin += fieldsToSave .map(f => camelToPostgres(f.field.name)) .join(','); queryEnd += fieldsToSave .map((f, id) => '$' + (id + 1)) .join(','); queryBegin += ')' queryEnd += ') RETURNING *;' let query = queryBegin + queryEnd; db .query(query, fieldsToSave.map(f => f.value)) .then(res => { this._setFromDbResult(res); this._persisted = true; callback(undefined, this); }) .catch(err => callback(err)); } else { // UPDATE FROM let query = 'UPDATE ' + camelToPostgres(model.name) + ' SET '; query += fieldsToSave.map((f, i) => { return camelToPostgres(f.field.name) + '=$' + (i+1); }).join(','); query += ' WHERE id=$' + (fieldsToSave.length + 1) + ' RETURNING *;' db .query(query, [...fieldsToSave.map(f => f.value), this.id]) .then(res => { this._setFromDbResult(res); this._persisted = true; callback(undefined, this); }) .catch(err => callback(err)); } } return ret; } module.exports.reinitialize = function(callback = () => {}) { let finished = models.map(m => false); for (let i = 0; i < models.length; i++) { let model = models[i]; model.reinitialize(() => { finished[i] = true; if (finished.reduce((a, b) => a && b, true)) { callback(); } }); } } module.exports.load = function(controllersDir) { fs.readdirSync(controllersDir).forEach(function(name) { let path = controllersDir + '/' + name + '/models.js'; try { fs.accessSync(path); } catch (e) { return; } require(path); }); }