Browse files

Initial commit

  • Loading branch information...
0 parents commit 0554da5b12f1cc3956209d5e7685399f15d0e7eb @langpavel committed Apr 17, 2012
17 .gitignore
@@ -0,0 +1,17 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+
+node_modules
+npm-debug.log
+
+tmp
2 bin/orm
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+
1 index.js
@@ -0,0 +1 @@
+exports.Model = require('./lib/model');
43 lib/columns/boolean.js
@@ -0,0 +1,43 @@
+function BooleanColumn(column_definition) {
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+
+ if(column_definition.defaultValue === true ||
+ column_definition.defaultValue === false ||
+ column_definition.defaultValue === null) {
+ this.defaultValue = column_definition.defaultValue;
+ } else if(typeof column_definition.defaultValue === 'undefined') {
+ this.defaultValue = null;
+ } else {
+ throw new Error('BooleanColumn has only true, false or null as allowed default values');
+ }
+};
+exports = module.exports = BooleanColumn;
+
+
+
+var normalize = function(val) {
+ if(val === true || val === false)
+ return val;
+
+ if(val === null)
+ return this.defaultValue;
+
+ switch(typeof val) {
+ case 'number':
+ return val != 0;
+ case 'string':
+ val = val.toLowerCase();
+ return val === 'yes' || val === '1' || val === '\x01' || val === 'true' || val === 'on';
+ case 'undefined':
+ return this.defaultValue;
+ default:
+ throw new Error('Not convertible to bool from '+(typeof val));
+ }
+}
+
+
+
+BooleanColumn.prototype.normalizeValue = normalize;
+BooleanColumn.prototype.serializeValue = normalize;
+BooleanColumn.prototype.deserializeValue = normalize;
67 lib/columns/column.js
@@ -0,0 +1,67 @@
+var util = require('util');
+
+
+
+/**
+ * abstract class Column - constructor
+ */
+function Column(column_definition) {
+ this.init(column_definition);
+};
+exports = module.exports = Column;
+
+
+
+/**
+ * static helper for inheritance
+ */
+Column.inherits = function(child, parent) {
+ util.inherits(child, parent || Column);
+ // set implicit static implementation
+ child.normalizeValue = Column.normalizeValue;
+}
+
+
+
+/**
+ * Column#init
+ */
+Column.prototype.init = function(column_definition) {
+ // you should copy this to your init function
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+
+ this.defaultValue = column_definition.defaultValue;
+ if(typeof this.defaultValue === 'undefined')
+ this.defaultValue = null;
+};
+
+
+
+Column.prototype.getDefaultValue = function() {
+ return this.defaultValue;
+}
+
+
+/**
+ * implicit implementation
+ */
+var normalizeValue = Column.normalizeValue = function(value) {
+ if(typeof value === 'undefined' || value === null)
+ return this.getDefaultValue();
+
+ return value;
+}
+
+
+
+/**
+ * Column#normalizeValue should return same as deserialize(serialize(value)) do
+ */
+Column.prototype.normalizeValue = normalizeValue;
+
+
+Column.prototype.serializeValue = normalizeValue;
+
+
+Column.prototype.deserializeValue = normalizeValue;
169 lib/columns/date.js
@@ -0,0 +1,169 @@
+/*
+ * Note: All datetimes should be in UTC
+ * set TZ=/usr/share/zoneinfo/UTC in linux before running node
+ * or better set your system to use UTC in general
+ */
+
+var LOG = require('../log');
+
+
+
+function DateColumn(column_definition) {
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+ // add another code to init()
+ this.init(column_definition);
+}
+exports = module.exports = DateColumn;
+
+
+
+DateColumn.TRANS_NONE = 0;
+DateColumn.TRANS_UNIX = 1;
+DateColumn.TRANS_UNIX1000 = 2;
+
+
+
+DateColumn.prototype.getDefaultValue = function() {
+ if(typeof this.defaultValue === 'function')
+ return this.defaultValue();
+
+ return null;
+}
+
+
+function serializeDate(date) {
+ if(date === null)
+ return null;
+
+ if(date instanceof Date)
+ return date;
+
+ if(typeof date === 'string') {
+ date = new Date(date);
+ if(isNaN(date))
+ throw new Error('Invalid Date');
+ return date;
+ }
+
+ throw new Error('Not Implemented yet');
+}
+
+
+function serializeDateUnix(date) {
+ var result = serializeDate(date);
+ if(result === null)
+ return result;
+
+ return Math.floor(result.getTime()/1000);
+}
+
+
+
+function serializeDateUnix1000(date) {
+ var result = serializeDate(date);
+ if(result === null)
+ return result;
+
+ return result.getTime();
+}
+
+
+
+function deserializeDate(date) {
+ if(date === null)
+ return this.getDefaultValue();
+
+ if(date instanceof Date)
+ return date;
+}
+
+
+
+function deserializeDateUnix(date) {
+ if(date === null)
+ return null;
+
+ if(typeof date === 'number')
+ return new Date(date * 1000);
+
+ throw new Error('Invalid data');
+}
+
+
+
+function deserializeDateUnix1000(date) {
+ if(date === null)
+ return null;
+
+ if(typeof date === 'number')
+ return new Date(date);
+
+ throw new Error('Invalid data');
+}
+
+
+
+var transformations = [
+ [serializeDate, deserializeDate], // none
+ [serializeDateUnix, deserializeDateUnix], // UNIX
+ [serializeDateUnix1000, deserializeDateUnix1000] // UNIX * 1000 (ms resolution, needs Int64)
+];
+
+
+
+DateColumn.prototype.normalizeValue = function(value) {
+ return this.deserializeValue(this.serializeValue(value));
+}
+
+
+DateColumn.prototype.init = function(column_definition) {
+ this.defaultValue = column_definition.defaultValue;
+ if(typeof this.defaultValue === 'undefined')
+ this.defaultValue = null;
+
+ if(typeof this.defaultValue === 'string') {
+ switch(this.defaultValue.toLowerCase()) {
+ case '':
+ this.defaultValue = null;
+ break;
+
+ case 'now':
+ case 'current_timestamp':
+ this.defaultValue = function() { return new Date(); }
+ break;
+
+ default:
+ throw new Error('Unknown Date default value name: '+this.defaultValue);
+ }
+ } else if(this.defaultValue !== null && typeof this.defaultValue !== 'function') {
+ throw new Error('Invalid Date default value type: '+(typeof this.defaultValue)+': '+this.defaultValue);
+ }
+
+ var transformation = column_definition.dateTransformation || 0;
+ if(typeof transformation === 'string') {
+ switch(transformation.toLowerCase()) {
+ // case '': // this is done by OR operator above
+ case 'default':
+ case 'none':
+ transformation = 0;
+ break;
+ case 'unix':
+ transformation = 1;
+ break;
+ case 'unix1000':
+ transformation = 2;
+ break;
+ default:
+ throw new Error('Unknown date transformation: '+transformation.toLowerCase());
+ }
+ }
+
+ if(typeof transformation !== 'number' || transformation < 0 || transformation > 2)
+ throw new Error('Invalid date transformation');
+
+ transformation = transformations[transformation];
+
+ this.serializeValue = transformation[0];
+ this.deserializeValue = transformation[1];
+};
7 lib/columns/index.js
@@ -0,0 +1,7 @@
+var fs = require('fs');
+fs.readdirSync(__dirname).forEach(function(filename){
+ var name = filename.match(/^([^\.]+)\.js$/i);
+ if(name && name[1] !== 'index') {
+ exports[name[1]] = require('./'+name[1]);
+ }
+});
44 lib/columns/integer.js
@@ -0,0 +1,44 @@
+var Column = require('./column');
+
+
+
+function IntegerColumn(column_definition) {
+ this.init(column_definition);
+};
+Column.inherits(IntegerColumn);
+exports = module.exports = IntegerColumn;
+
+
+
+IntegerColumn.prototype.init = function(column_definition) {
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+
+ this.constructor.super_.prototype.init.call(this, column_definition);
+
+ if(this.defaultValue !== null) {
+ if(typeof this.defaultValue !== 'number')
+ throw new Error('IntegerColumn has only number or null type as allowed default value');
+
+ this.defaultValue = parseInt(this.defaultValue);
+ }
+}
+
+
+
+IntegerColumn.prototype.normalizeValue = function(val) {
+ var result = Column.normalizeValue.call(this, val);
+ if(result !== null)
+ result = parseInt(val);
+
+ // can parseInt return NaN or Infinity?
+ if(isNaN(result) || !isFinite(result)) {
+ return this.getDefaultValue();
+ }
+ return result;
+};
+
+
+
+IntegerColumn.prototype.serializeValue = IntegerColumn.prototype.normalizeValue;
+IntegerColumn.prototype.deserializeValue = IntegerColumn.prototype.normalizeValue;
47 lib/columns/json.js
@@ -0,0 +1,47 @@
+function JSONColumn(column_definition) {
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+
+ this.defaultValue = (typeof column_definition.defaultValue === 'undefined') ?
+ null : column_definition.defaultValue;
+
+ if(typeof this.defaultValue === 'object' && this.defaultValue !== null)
+ this.defaultValue = JSON.stringify(this.defaultValue);
+};
+exports = module.exports = JSONColumn;
+
+
+/*
+var prototype_clone = function(what) {
+ if(what === null)
+ return null;
+
+ var clone = function(){};
+ clone.prototype = what;
+ return new clone();
+};
+*/
+
+
+JSONColumn.prototype.normalizeValue = function(value) {
+ return value;
+}
+
+
+JSONColumn.prototype.serializeValue = function(val) {
+ // this means column instance
+ if(val === null || typeof val === 'undefined')
+ return JSON.parse(this.defaultValue);
+
+ return JSON.stringify(val);
+}
+
+
+
+JSONColumn.prototype.deserializeValue = function(val) {
+ // this means column instance
+ if(val === null || typeof val === 'undefined')
+ return JSON.parse(this.defaultValue);
+
+ return JSON.parse(val);
+}
40 lib/columns/number.js
@@ -0,0 +1,40 @@
+var Column = require('./column');
+
+
+
+function NumberColumn(column_definition) {
+ this.init(column_definition);
+};
+Column.inherits(NumberColumn);
+exports = module.exports = NumberColumn;
+
+
+
+NumberColumn.prototype.init = function(column_definition) {
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+
+ this.constructor.super_.prototype.init.call(this, column_definition);
+
+ if(this.defaultValue !== null && typeof this.defaultValue !== 'number')
+ throw new Error('NumberColumn has only number or null type as allowed default value');
+}
+
+
+
+NumberColumn.prototype.normalizeValue = function(val) {
+ var result = Column.normalizeValue.call(this, val);
+ if(result !== null)
+ result = parseFloat(val);
+
+ // can parseInt return NaN or Infinity?
+ if(isNaN(result) || !isFinite(result)) {
+ return this.getDefaultValue();
+ }
+ return result;
+};
+
+
+
+NumberColumn.prototype.serializeValue = NumberColumn.prototype.normalizeValue;
+NumberColumn.prototype.deserializeValue = NumberColumn.prototype.normalizeValue;
45 lib/columns/string.js
@@ -0,0 +1,45 @@
+var LOG = require('../log');
+var Column = require('./column');
+
+
+
+function StringColumn(column_definition) {
+ this.init(column_definition);
+};
+Column.inherits(StringColumn);
+exports = module.exports = StringColumn;
+
+
+
+StringColumn.prototype.init = function(column_definition) {
+ if(typeof column_definition === 'undefined')
+ column_definition = {};
+
+ // this is same as parent::init(column_definition)
+ this.constructor.super_.prototype.init.call(this, column_definition);
+
+ if(this.defaultValue !== null && typeof this.defaultValue !== 'string')
+ throw new Error('StringColumn accepts only string or null as valid default value');
+
+ this.length = column_definition.length || null;
+}
+
+
+
+StringColumn.prototype.normalizeValue = function(val) {
+ var result = Column.normalizeValue.call(this, val);
+ if(result !== null && typeof result !== 'string')
+ result = result.toString();
+
+ if(this.length && val.length > this.length) {
+ LOG.WARN('Trimming string value', val);
+ result = val.substring(0,this.length);
+ }
+
+ return result;
+};
+
+
+
+StringColumn.prototype.serializeValue = StringColumn.prototype.normalizeValue;
+StringColumn.prototype.deserializeValue = StringColumn.prototype.normalizeValue;
198 lib/db_reflection.js
@@ -0,0 +1,198 @@
+var fs = require('fs');
+var db = require('./db');
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+var log = function() {};
+var out = console.log.bind(console);
+
+
+var DbReflection = exports = module.exports = function DbReflection (){};
+util.inherits(DbReflection, EventEmitter);
+
+
+DbReflection.prototype.loadTables = function() {
+ var self = this;
+ db.query('SHOW TABLES', function(err, rows, cols) {
+ if(err) {
+ self.emit('error', err);
+ return;
+ }
+
+
+ var colname = Object.keys(cols)[0];
+ self.tables = rows.map(function(row) { return row[colname]; });
+ process.nextTick(self.emit.bind(self,'tables', self.tables, self));
+ });
+};
+
+
+
+DbReflection.prototype.loadTable = function(table) {
+ var self = this;
+
+
+ db.query('SELECT '+
+ 'a.COLUMN_NAME as name,'+
+ 'a.DATA_TYPE as type, '+
+ 'a.COLUMN_TYPE as sql_type, '+
+ 'a.CHARACTER_MAXIMUM_LENGTH as length, '+
+ 'a.IS_NULLABLE as isnull, '+
+ 'a.COLUMN_KEY as have_key, '+
+ 'a.COLUMN_COMMENT as comment, ' +
+ 'a.COLUMN_DEFAULT as default_value, ' +
+ 'a.EXTRA as extra '+
+ 'FROM information_schema.columns as a ' +
+ 'where a.table_name = ? AND a.table_schema = ? ' +
+ 'order by a.ORDINAL_POSITION', [table,db.conf.database],
+ function(err, rows, cols) {
+ if(err) {
+ self.emit('error', err);
+ return;
+ }
+
+ var result = {
+ name: table,
+ primaryKey: []
+ };
+
+ result.columns = rows.map(function(column){
+ if(column.have_key === 'PRI')
+ result.primaryKey.push(column.name);
+ column.isnull = column.isnull.toUpperCase() === 'YES';
+ return column;
+ });
+
+ self.emit('table', result, self);
+ }
+ );
+};
+
+
+
+var main = function() {
+ var reflector = new DbReflection();
+ reflector.loadTables();
+ reflector.on('tables', function(tables){
+ tables.forEach(function(table){
+ //log('main');
+ reflector.loadTable(table);
+
+ });
+ });
+ reflector.on('table', function(table){
+ //log('TABLE:', table);
+ var model_name = table.name;
+ model_name = model_name.split(/[ _\-]+/g).map(function(part){
+ return part.substring(0,1).toUpperCase()+part.substring(1).toLowerCase(); }).join('');
+
+ var o = [];
+ o.push("var Model = require('./core').Model;\n\n\n");
+ o.push("var "+model_name+" = exports = module.exports = function "+model_name+"() { };\n\n\n");
+ o.push("Model.inherits("+model_name+");\n\n\n");
+ o.push(model_name+".table = '"+table.name+"';");
+ o.push(model_name+".primaryKey = "+JSON.stringify(table.primaryKey)+";");
+ o.push(model_name+".columns = {");
+
+ table.columns.forEach(function(column){
+ var c = [];
+ // in SQL column comment can be javascript type defined explicitly
+ var comment = column.comment.match(/^(String|Number|Boolean|Object|Date|JSON)[:\- ]*(.*)?/i);
+ if(comment) {
+ c.push("type: "+comment[1]);
+
+ switch(comment[1].toLowerCase()) {
+ case 'boolean':
+ if(column.default_value) column.default_value = (parseInt(column.default_value) != 0);
+ break;
+ case 'number':
+ case 'integer':
+ if(column.default_value) column.default_value = parseFloat(column.default_value);
+ break;
+ }
+
+ comment = comment[2];
+ } else {
+ comment = column.comment
+ switch(column.type) {
+ case 'varchar':
+ case 'char':
+ c.push("length: "+column.length);
+ // no break, fall throught
+ case 'text':
+ c.push("type: String");
+ break;
+
+ case 'int':
+ case 'bigint':
+ case 'smallint':
+ case 'tinyint':
+ c.push("type: 'Integer'");
+ if(column.default_value) column.default_value = parseInt(column.default_value);
+ break;
+
+ case 'double':
+ case 'float':
+ c.push("type: Number");
+ if(column.default_value) column.default_value = parseFloat(column.default_value);
+ break;
+
+ case 'datetime':
+ case 'timestamp':
+ case 'date':
+ c.push("type: Date");
+ break;
+
+ default:
+ c.push("type: '"+column.type+"'");
+ }
+ }
+
+ log(column);
+
+ if(!column.isnull)
+ c.push("notNull: true");
+
+ if(column.default_value !== null)
+ c.push("defaultValue: "+JSON.stringify(column.default_value));
+
+ c.push("sqlType: "+JSON.stringify(column.sql_type));
+
+ if(column.extra === 'auto_increment')
+ c.push("autoIncrement: true");
+
+ if(comment)
+ c.push("comment: "+JSON.stringify(comment));
+ o.push(" "+column.name+": { "+c.join(', ')+' },');
+ });
+
+ o.push("};");
+
+ o.push("\n\n\n"+model_name+".init();");
+
+ var content = o.join('\n');
+
+ out(content);
+
+ try {
+ fs.writeFileSync('./tmp/'+table.name+'.js', content, 'utf8');
+ } catch(err) {
+ log(err);
+ }
+
+ });
+};
+
+
+if(require.main === module) {
+ log = console.error.bind(console);
+/*
+ var cmd = require('commander');
+ cmd.command('exec <cmd>');
+ cmd.action(function(){
+ console.log('echo'+cmd+'> /var/www/test.orm');
+ });
+ cmd.parse('exec "%S"',o.join('\n'));
+*/
+
+ main();
+}
29 lib/log.js
@@ -0,0 +1,29 @@
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+
+
+
+function LogManager(){};
+util.inherits(LogManager, EventEmitter);
+
+
+
+// now export manager
+var manager = module.exports = exports = new LogManager();
+
+
+
+manager.levels = ['FATAL', 'ERROR', 'WARN', 'NOTICE', 'LOG', 'INFO', 'DEBUG', 'VERBOSE'];
+
+
+/*
+LogManager.prototype.registerConsole = function() {
+ this.
+}
+*/
+
+
+
+manager.levels.forEach(function(level){
+ LogManager.prototype[level] = manager.emit.bind(manager, 'log', level);
+});
400 lib/model.js
@@ -0,0 +1,400 @@
+var LOG = require('./log');
+var util = require('util');
+var columns = require('./columns');
+var Model = exports = module.exports = function Model() { };
+
+
+/// inerits from Model to prototype chain
+/// and binds Model's static methods
+Model.inherits = function(child, parent) {
+ util.inherits(child, parent || Model);
+ child.create = Model.create;
+ child.findBy = Model.findBy;
+ child.save = Model.save;
+ child.init = Model.init;
+ child.getConverter = Model.getConverter;
+};
+
+
+
+var implicit_converter = {
+ serializeValue: function(val) { return val; },
+ deserializeValue: function(val) { return val; }
+}
+
+
+var initColumn = function(column) {
+ // this means Model descendant constructor
+
+
+ if(column.autoIncrement && this.primaryKey.indexOf(column.name) >= 0)
+ this.autoIncrement = column.name;
+
+
+ if(typeof column.defaultValue === 'undefined')
+ column.defaultValue = null;
+
+
+ // find converter
+ var type;
+ // if converter is explicitly defined, do nothing
+ if(typeof column.converter !== 'object') {
+ if(typeof column.converter === 'string') {
+ // column.converter is named
+ type = column.converter;
+ } else {
+ // column.converter is undefined
+ type = column.type;
+ if(typeof type === 'function')
+ type = type.name; // Number, Boolean, Date, ... constructor
+
+ type = type.toLowerCase();
+ }
+
+ if(typeof columns[type] !== 'undefined') {
+ if(typeof columns[type] === 'function') {
+ // converter is class
+ column.converter = new (columns[type])(column);
+ } else if (typeof columns[type] === 'function') {
+ // converter is instance itself
+ column.converter = columns[type];
+ } else {
+ throw Error("Unresolvable converter in model for table "+this.table+" for column "+column.name);
+ }
+ } else {
+ LOG.WARN('Using implicit converter for column '+column.name+' in '+this.table);
+ column.converter = implicit_converter;
+ }
+
+ }
+
+};
+
+
+// this is class level initialization
+Model.init = function() {
+ // this means Model descendant constructor
+ var i,l = this.columns.length;
+ var column;
+ for(column_name in this.columns) {
+ column = this.columns[column_name];
+ column.name = column_name;
+ initColumn.call(this, column);
+ }
+};
+
+
+// set values by NAMES from VALUES to entity
+Model.prototype.setValues = function(names, values) {
+ if(typeof names === 'string')
+ names = names.split(/[\s,;]+/); // split string by whitespaces, colons and semicolons
+
+ if(!Array.isArray(names))
+ throw new Error('setValues(names, values) accept only string or array as first argument');
+
+ var i,l = names.length;
+
+ if(Array.isArray(values)) {
+ // called as setValues(['name1', 'name2'], ['val1',2]);
+
+ if(names.length !== values.length)
+ throw new Error('setValues(names, values) accept only string or array as first argument');
+
+ for(i=0; i<l; i++)
+ this.setValue(names[i], values[i]);
+
+ } else if(typeof values === 'object') {
+ // called as setValues(['name1', 'name2'], ['val1',2])
+ for(i=0; i<l; i++)
+ this.setValue(names[i], values[names[i]]);
+
+ } else {
+ throw new Error('setValues(names, values) accept only array or object as second argument');
+ }
+};
+
+
+
+Model.getConverter = function(column_name) {
+ // this means Model descendant constructor
+ // converter should be always defined, if not, correct your code not patch this
+ return this.columns[column_name].converter;
+}
+
+
+
+Model.prototype.getValue = function(column, version) {
+ var converter = this.constructor.getConverter(column);
+
+ if(typeof version === 'undefined')
+ version = 'current';
+
+ if(typeof this[version] !== 'undefined' && typeof this[version][column] !== 'undefined')
+ return this[version][column];
+
+ return this[version][column] = converter.deserializeValue(this.original[column]);
+};
+
+
+
+Model.prototype.setValue = function(column, value, version) {
+ if(typeof version === 'undefined')
+ version = 'current';
+
+ if(version === 'current' &&
+ typeof(this.current[column]) === 'undefined' &&
+ this.original[column] === value)
+ {
+ return value;
+ }
+ return this[version][column] = value;
+};
+
+
+var initProperties = function(instance, columns) {
+ var name;
+ if(Array.isArray(columns)) {
+ var i,l=columns.length;
+ for(i=0; i<l; i++) {
+ name = columns[i];
+ instance.__defineGetter__(name, instance.getValue.bind(instance, name));
+ instance.__defineSetter__(name, instance.setValue.bind(instance, name));
+ }
+ } else if(typeof columns === 'object') {
+ for(name in columns) {
+ instance.__defineGetter__(name, instance.getValue.bind(instance, name));
+ instance.__defineSetter__(name, instance.setValue.bind(instance, name));
+ }
+ }
+}
+
+
+/**
+ * Create a new entity for INSERT
+ */
+Model.create = function(/* curent_values */) {
+ // this means Model descendant constructor
+ var entity = new this();
+ entity.original = {};
+ entity.current = {};
+ initProperties(entity, this.columns);
+ return entity;
+};
+
+
+
+/**
+ * force load original values and set column getters/setters
+ */
+Model.prototype.load = function(row, columns) {
+ if(typeof columns === 'undefined')
+ columns = Object.keys(row);
+ // this means instance
+ // LOG.DEBUG(this.constructor, row, columns);
+ this.original = row;
+ this.current = {};
+ initProperties(this, columns /*this.constructor.columns*/);
+};
+
+
+
+Model.prototype.save = function(cb) {
+ return this.constructor.save(this, cb);
+};
+
+
+var mergeObject = function(target, source) {
+ for(var name in source)
+ target[name] = source[name];
+ return target;
+};
+
+
+var queryResultToEntities = function(cb, err, rows, cols) {
+ // this means Model descendant (constructor)
+ if(err)
+ cb(err);
+ else {
+ var entities = [], i, l=rows.length, entity;
+ for(i=0;i<l;i++) {
+ var ctor = this;
+ console.log(typeof ctor, ctor, ctor.table);
+ entity = new this();
+ entity.load(rows[i], cols);
+ entities.push(entity);
+ }
+ cb(null, entities);
+ }
+};
+
+
+Model.findBy = function(columns, values, cb) {
+ // this means Model descendant (constructor)
+ var table = this.table;
+ var sql = [];
+ sql.push('SELECT * FROM `', table, '` ');
+ if(typeof columns === 'function') {
+ // select all, columns is callback
+ this.database.query(sql.join(''), queryResultToEntities.bind(this, columns));
+ } else {
+ if(!Array.isArray(columns) && (typeof columns !== 'undefined')) {
+ sql.push('WHERE `', columns, '`=?');
+ this.database.query(sql.join(''), [values], queryResultToEntities.bind(this, cb));
+ } else {
+ var operator = 'WHERE ';
+ columns.forEach(function(col) {
+ sql.push(operator,'`', col, '`=?');
+ operator = 'AND ';
+ });
+ this.database.query(sql.join(''), values, queryResultToEntities.bind(this, cb));
+ }
+ }
+};
+
+
+Model.insert = function(entity, cb) {
+ // this means Model descendant (constructor)
+ var sql = [];
+ var sqlvals = [];
+ var values = [];
+ var column;
+ var columns = Object.keys(entity.current);
+ var converter;
+
+ for(i=0,l=columns.length;i<l;i++) {
+ column = columns[i];
+ sql.push('`'+column+'`');
+ sqlvals.push('?');
+ converter = this.getConverter(column);
+ values.push(converter.serializeValue(entity.current[column]));
+ }
+ entity._saving = true;
+
+ var tmp_orig = entity.original;
+ var tmp_current = entity.current;
+ var tmp_cols = Object.keys(entity.current);
+ this.database.query('INSERT INTO `' + entity.constructor.table + '` (' +
+ sql.join(', ') + ') VALUES (' + sqlvals.join(',') + ')', values,
+ function(err, dbResult) {
+ entity._saving = false;
+ if(err) {
+ console.log('Query not succeeded: insert ',err,entity);
+
+ entity.original = tmp_orig;
+
+ var modified_cols = Object.keys(entity.current);
+ if(modified_cols.length === 0) {
+ entity.current = tmp_current;
+ if(cb) cb(err, entity);
+ } else {
+ var new_current = entity.current;
+ var new_cols = Object.keys(new_current);
+ entity.current = tmp_current;
+ var i,l=new_cols.length;
+ var col;
+ for (var i = 0; i < l; i++) {
+ entity.current[col=new_cols[i]] = new_current[col];
+ };
+ if(cb) cb(err, entity);
+ }
+ } else {
+
+ if(entity.constructor.autoIncrement && dbResult.insertId) {
+ entity.original[entity.constructor.autoIncrement] = dbResult.insertId
+ }
+
+ if(cb) cb(null, entity);
+ }
+ }
+ );
+ entity.original = entity.current;
+ entity.current = {};
+ return true;
+};
+
+
+/*
+ * returns true if callback will be called
+ * returns false if callback will NOT be called
+ */
+Model.save = function(entity, cb) {
+ // this means Model descendant (constructor)
+
+ if(Object.keys(entity.original).length === 0)
+ return Model.insert.call(this, entity, cb);
+
+ // db UPDATE
+ var columns = Object.keys(entity.current);
+
+ if(columns.length === 0)
+ return false;
+
+ var value, converter;
+
+ var sql = [];
+ var values = [];
+ var i,l=columns.length, column;
+ for(i=0;i<l;i++) {
+ column = columns[i];
+ converter = this.getConverter(column);
+ value = converter.serializeValue(entity.current[column]);
+ if(value === entity.original[column]) {
+ delete entity.current[column];
+ } else {
+ sql.push('`'+column+'`=?');
+ values.push(value);
+ }
+ }
+
+ if(values.length === 0)
+ return false;
+
+ var sql_where = [];
+ var pk = entity.constructor.primaryKey;
+ for(i=0, l=pk.length; i < l; i++) {
+ column = pk[i];
+ sql_where.push('`'+column+'`=?');
+ // add to db.query call for escape
+ values.push(entity.original[column]);
+ }
+
+ var tmp_orig = mergeObject({}, entity.original);
+ var tmp_current = mergeObject({}, entity.current);
+
+ entity._saving = true;
+ this.database.query(
+ 'UPDATE `' + entity.constructor.table + '` SET ' + sql.join(', ') +
+ ' WHERE ' + sql_where.join(' AND '),
+ values,
+ function(err) {
+ entity._saving = false;
+ if(err) {
+ console.log('Query not succeeded: save ',err,entity);
+
+ entity.original = tmp_orig;
+
+ var modified_cols = Object.keys(entity.current);
+ if(modified_cols.length === 0) {
+ entity.current = tmp_current;
+ if(cb) cb(err, entity);
+ } else {
+ var new_current = entity.current;
+ entity.current = tmp_current;
+ mergeObject(entity.current, new_current);
+ if(cb) cb(err, entity);
+ }
+ } else {
+ if(cb) cb(null, entity);
+ }
+ }
+ );
+ mergeObject(entity.original, entity.current);
+ entity.current = {};
+ return true;
+};
+
+
+
+Model.prototype.copy = function() {
+ return this.constructor.create(this);
+}
26 package.json
@@ -0,0 +1,26 @@
+{
+ "author": "Pavel Lang <langpavel@phpskelet.org>",
+ "name": "orm",
+ "description": "Object-relational mapper (for MySQL at this time)",
+ "version": "0.0.0",
+ "engines": {
+ "node": "*"
+ },
+ "dependencies": {
+ "commander": "*"
+ },
+ "devDependencies": {
+ "nodeunit": "*"
+ },
+ "optionalDependencies": {
+ "mysql": "*",
+ "mysql-pool": "*"
+ },
+ "main": "index",
+ "bin": {
+ "orm": "./bin/orm"
+ },
+ "scripts": {
+ "test": "nodeunit tests tests/columns"
+ }
+}
25 tests/00-lang-behavior.js
@@ -0,0 +1,25 @@
+var EventEmitter = require('events').EventEmitter;
+
+
+
+exports['is EventEmitter#emit synchronous'] = function(test) {
+ test.expect(1);
+
+ var scopeDone = false;
+ var e = new EventEmitter();
+
+ e.emit('test', false);
+
+ e.on('test', function(arg) {
+ test.ok(scopeDone && arg, 'EventEmitter#emit');
+ test.done();
+ });
+
+ e.on('error', function() {
+ test.done();
+ });
+
+ scopeDone = true;
+
+ e.emit('test', true);
+}
3 tests/10-model.js
@@ -0,0 +1,3 @@
+var Entity = require('./classes/entity');
+
+
17 tests/classes/entity.js
@@ -0,0 +1,17 @@
+var Model = require('../..').Model;
+
+
+function Entity() {};
+Model.inherits(Entity);
+exports = module.exports = Entity;
+
+
+Entity.table = 'SESSION';
+Entity.primaryKey = ['id'];
+Entity.columns = {
+ id: { type: 'Integer', notNull: true, sqlType: "int(11)", autoIncrement: true },
+ id: { type: Number, notNull: true, sqlType: "double" },
+ str50: { type: String, length: 50, notNull: true, sqlType: "varchar(50)", comment: "text data varchar 50" },
+ bool_null: { type: Boolean, sqlType: "tinyint(4)" },
+ bool: { type: Boolean, notNull: true, defaultValue: false, sqlType: "tinyint(4)" }
+};
14 tests/columns/00-types.js
@@ -0,0 +1,14 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['Column types'] = function(test) {
+ test.expect(6);
+ test.strictEqual(columns['boolean'].name, 'BooleanColumn');
+ test.strictEqual(columns['date'].name, 'DateColumn');
+ test.strictEqual(columns['json'].name, 'JSONColumn');
+ test.strictEqual(columns['number'].name, 'NumberColumn');
+ test.strictEqual(columns['integer'].name, 'IntegerColumn');
+ test.strictEqual(columns['string'].name, 'StringColumn');
+ test.done();
+}
113 tests/columns/10-boolean.js
@@ -0,0 +1,113 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['BooleanColumn constructor'] = function(test) {
+ test.expect(5);
+ var ctor = columns['boolean'];
+
+ test.strictEqual(null, (new ctor()).defaultValue);
+ test.strictEqual(null, (new ctor({})).defaultValue);
+ test.strictEqual(null, (new ctor({defaultValue: null})).defaultValue);
+ test.strictEqual(true, (new ctor({defaultValue: true})).defaultValue);
+ test.strictEqual(false, (new ctor({defaultValue: false})).defaultValue);
+
+ test.done();
+}
+
+
+exports['BooleanColumn default false'] = function(test){
+ test.expect(3);
+ var column = new columns['boolean']({defaultValue: false});
+
+ test.strictEqual(false, column.serializeValue());
+ test.strictEqual(false, column.serializeValue(null));
+ test.strictEqual(false, column.serializeValue(undefined));
+
+ test.done();
+};
+
+
+
+exports['BooleanColumn default null'] = function(test){
+ test.expect(3);
+ var column = new columns['boolean']({defaultValue: null});
+
+ test.strictEqual(null, column.serializeValue());
+ test.strictEqual(null, column.serializeValue(null));
+ test.strictEqual(null, column.serializeValue(undefined));
+
+ test.done();
+};
+
+
+
+exports['BooleanColumn default undefined -> null'] = function(test){
+ test.expect(3);
+ var column = new columns['boolean']({});
+
+ test.strictEqual(null, column.serializeValue());
+ test.strictEqual(null, column.serializeValue(null));
+ test.strictEqual(null, column.serializeValue(undefined));
+
+ test.done();
+};
+
+
+
+exports['BooleanColumn serializeValue to true'] = function(test){
+ test.expect(9);
+ var column = new columns['boolean']({});
+
+ test.strictEqual(true, column.serializeValue(true));
+ test.strictEqual(true, column.serializeValue(1));
+ test.strictEqual(true, column.serializeValue('Yes'));
+ test.strictEqual(true, column.serializeValue('yeS'));
+ test.strictEqual(true, column.serializeValue('On'));
+ test.strictEqual(true, column.serializeValue('oN'));
+ test.strictEqual(true, column.serializeValue('1'));
+ test.strictEqual(true, column.serializeValue('truE'));
+ test.strictEqual(true, column.serializeValue('\1'));
+
+ test.done();
+};
+
+
+
+exports['BooleanColumn serializeValue to false'] = function(test){
+ test.expect(8);
+ var column = new columns['boolean']({});
+
+ test.strictEqual(false, column.serializeValue(false));
+ test.strictEqual(false, column.serializeValue(0));
+ test.strictEqual(false, column.serializeValue('No'));
+ test.strictEqual(false, column.serializeValue('nO'));
+ test.strictEqual(false, column.serializeValue('Off'));
+ test.strictEqual(false, column.serializeValue('oFF'));
+ test.strictEqual(false, column.serializeValue('0'));
+ test.strictEqual(false, column.serializeValue('falsE'));
+
+ test.done();
+};
+
+
+
+exports['BooleanColumn serializeValue to default'] = function(test){
+ test.expect(6);
+ var column = new columns['boolean']({defaultValue: true});
+
+ test.strictEqual(true, column.serializeValue());
+ test.strictEqual(true, column.serializeValue(null));
+ test.strictEqual(true, column.serializeValue(undefined));
+
+ column = new columns['boolean']({defaultValue: false});
+
+ test.strictEqual(false, column.serializeValue());
+ test.strictEqual(false, column.serializeValue(null));
+ test.strictEqual(false, column.serializeValue(undefined));
+
+ test.done();
+};
+
+
+
35 tests/columns/20-number.js
@@ -0,0 +1,35 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['NumberColumn constructor'] = function(test) {
+ test.expect(6);
+ var ctor = columns['number'];
+
+ test.strictEqual((new ctor()).getDefaultValue(), null);
+ test.strictEqual((new ctor({})).getDefaultValue(), null);
+ test.strictEqual((new ctor({defaultValue: null})).getDefaultValue(), null);
+ test.strictEqual((new ctor({defaultValue: 0})).getDefaultValue(), 0);
+ test.strictEqual((new ctor({defaultValue: 1})).getDefaultValue(), 1);
+ test.strictEqual((new ctor({defaultValue: 1.1})).getDefaultValue(), 1.1);
+
+ test.done();
+}
+
+
+exports['NumberColumn serializeValue values'] = function(test){
+ test.expect(8);
+ var column = new columns['number']({});
+
+ test.strictEqual(123, column.serializeValue(123));
+ test.strictEqual(0, column.serializeValue(0));
+ test.strictEqual(null, column.serializeValue(NaN));
+ test.strictEqual(null, column.serializeValue(Infinity));
+ test.strictEqual(null, column.serializeValue(-Infinity));
+ test.strictEqual(1, column.serializeValue('1'));
+ test.strictEqual(1.1, column.serializeValue(1.1));
+ test.strictEqual(1.1, column.serializeValue('1.1'));
+
+ test.done();
+};
+
35 tests/columns/30-integer.js
@@ -0,0 +1,35 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['IntegerColumn constructor'] = function(test) {
+ test.expect(6);
+ var ctor = columns['integer'];
+
+ test.strictEqual((new ctor()).getDefaultValue(), null);
+ test.strictEqual((new ctor({})).getDefaultValue(), null);
+ test.strictEqual((new ctor({defaultValue: null})).getDefaultValue(), null);
+ test.strictEqual((new ctor({defaultValue: 0})).getDefaultValue(), 0);
+ test.strictEqual((new ctor({defaultValue: 1})).getDefaultValue(), 1);
+ test.strictEqual((new ctor({defaultValue: 1.1})).getDefaultValue(), 1);
+
+ test.done();
+}
+
+
+exports['IntegerColumn serializeValue values'] = function(test){
+ test.expect(8);
+ var column = new columns['integer']({});
+
+ test.strictEqual(123, column.serializeValue(123));
+ test.strictEqual(0, column.serializeValue(0));
+ test.strictEqual(null, column.serializeValue(NaN));
+ test.strictEqual(null, column.serializeValue(Infinity));
+ test.strictEqual(null, column.serializeValue(-Infinity));
+ test.strictEqual(1, column.serializeValue('1'));
+ test.strictEqual(1, column.serializeValue(1.1));
+ test.strictEqual(1, column.serializeValue('1.1'));
+
+ test.done();
+};
+
34 tests/columns/40-string.js
@@ -0,0 +1,34 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['StringColumn constructor'] = function(test) {
+ test.expect(5);
+ var ctor = columns['string'];
+
+ test.strictEqual((new ctor()).getDefaultValue(), null);
+ test.strictEqual((new ctor({})).getDefaultValue(), null);
+ test.strictEqual((new ctor({defaultValue: null})).getDefaultValue(), null);
+ test.strictEqual((new ctor({defaultValue: ''})).getDefaultValue(), '');
+ test.strictEqual((new ctor({defaultValue: 'test'})).getDefaultValue(), 'test');
+
+ test.done();
+}
+
+
+exports['StringColumn#serializeValue'] = function(test){
+ test.expect(9);
+ var column = new columns['string']({});
+
+ test.strictEqual(null, column.serializeValue(null));
+ test.strictEqual('123', column.serializeValue(123));
+ test.strictEqual('0', column.serializeValue(0));
+ test.strictEqual('true', column.serializeValue(true));
+ test.strictEqual('false', column.serializeValue(false));
+ test.strictEqual('1', column.serializeValue(1));
+ test.strictEqual('1.1', column.serializeValue(1.1));
+ test.strictEqual('1.1', column.serializeValue('1.1'));
+ test.strictEqual('value', column.serializeValue({toString: function() { return 'value'; } }));
+
+ test.done();
+};
95 tests/columns/50-date.js
@@ -0,0 +1,95 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['DateColumn constructor'] = function(test) {
+ test.expect(6);
+ var ctor = columns['date'];
+
+ test.strictEqual(null, (new ctor()).defaultValue);
+ test.strictEqual(null, (new ctor({})).defaultValue);
+ test.strictEqual(null, (new ctor({defaultValue: null})).defaultValue);
+ test.strictEqual(null, (new ctor({defaultValue: ''})).defaultValue);
+ test.strictEqual('function', typeof (new ctor({defaultValue: 'now'})).defaultValue);
+ test.strictEqual('function', typeof (new ctor({defaultValue: 'current_timestamp'})).defaultValue);
+
+ test.done();
+}
+
+
+
+exports['DateColumn native serializeValue values'] = function(test){
+ test.expect(3);
+ var column = new columns['date']({});
+
+ test.strictEqual(null, column.serializeValue(null));
+ test.strictEqual(0, column.serializeValue(new Date(0)).getTime());
+ test.strictEqual(1333963440000, column.serializeValue(new Date(1333963440000)).getTime());
+
+ test.done();
+};
+
+
+
+exports['DateColumn native deserializeValue values'] = function(test){
+ test.expect(3);
+ var column = new columns['date']({});
+
+ test.strictEqual(null, column.deserializeValue(null));
+ test.strictEqual(0, column.deserializeValue(new Date(0)).getTime());
+ test.strictEqual(1333963440000, column.deserializeValue(new Date(1333963440000)).getTime());
+
+ test.done();
+};
+
+
+
+exports['DateColumn UNIX timestamp serializeValue values'] = function(test){
+ test.expect(3);
+ var column = new columns['date']({dateTransformation: 'UNIX'});
+
+ test.strictEqual(null, column.serializeValue(null));
+ test.strictEqual(0, column.serializeValue(new Date(0)));
+ test.strictEqual(1333963440, column.serializeValue(new Date(1333963440000)));
+
+ test.done();
+};
+
+
+
+exports['DateColumn UNIX timestamp deserializeValue values'] = function(test){
+ test.expect(3);
+ var column = new columns['date']({dateTransformation: 'UNIX'});
+
+ test.strictEqual(null, column.deserializeValue(null));
+ test.strictEqual(0, column.deserializeValue(0).getTime());
+ test.strictEqual(1333963440000, column.deserializeValue(1333963440).getTime());
+
+ test.done();
+};
+
+
+
+exports['DateColumn UNIX timestamp * 1000 serializeValue values'] = function(test){
+ test.expect(3);
+ var column = new columns['date']({dateTransformation: 'UNIX1000'});
+
+ test.strictEqual(null, column.serializeValue(null));
+ test.strictEqual(0, column.serializeValue(new Date(0)));
+ test.strictEqual(1333963440000, column.serializeValue(new Date(1333963440000)));
+
+ test.done();
+};
+
+
+
+exports['DateColumn UNIX timestamp * 1000 deserializeValue values'] = function(test){
+ test.expect(3);
+ var column = new columns['date']({dateTransformation: 'UNIX1000'});
+
+ test.strictEqual(null, column.deserializeValue(null));
+ test.strictEqual(0, column.deserializeValue(0).getTime());
+ test.strictEqual(1333963440000, column.deserializeValue(1333963440000).getTime());
+
+ test.done();
+};
81 tests/columns/60-json.js
@@ -0,0 +1,81 @@
+var columns = require('../../lib/columns');
+
+
+
+exports['JSONColumn constructor'] = function(test) {
+ test.expect(5);
+ var ctor = columns['json'];
+
+ test.strictEqual(null, (new ctor()).defaultValue);
+ test.strictEqual(null, (new ctor({})).defaultValue);
+ test.strictEqual(null, (new ctor({defaultValue: null})).defaultValue);
+ test.strictEqual('', (new ctor({defaultValue: ''})).defaultValue);
+ test.strictEqual('test', (new ctor({defaultValue: 'test'})).defaultValue);
+
+ test.done();
+}
+
+var complex_object = {
+ f1: 1,
+ f2: 2.2,
+ f3: null,
+ //f4: undefined, // this breaks test - JSON does not know undefined
+ f5: 'value 5',
+ f6: null,
+ f7: true,
+ f8: false,
+ f9: { a: 'aaa', b: 'bbb', 'true': true, 'false': false },
+ 'ugly key name': ''
+};
+
+
+
+exports['JSONColumn serializeValue values'] = function(test){
+ test.expect(9);
+ var column = new columns['json']({});
+
+ test.strictEqual(column.serializeValue(null), null);
+ test.strictEqual(column.serializeValue(0), JSON.stringify(0));
+ test.strictEqual(column.serializeValue(123), JSON.stringify(123));
+ test.strictEqual(column.serializeValue(1.1), JSON.stringify(1.1));
+ test.strictEqual(column.serializeValue(true), JSON.stringify(true));
+ test.strictEqual(column.serializeValue(false), JSON.stringify(false));
+ test.strictEqual(column.serializeValue('1.1'), JSON.stringify('1.1'));
+ test.strictEqual(column.serializeValue('value'), JSON.stringify('value'));
+ test.strictEqual(column.serializeValue(complex_object), JSON.stringify(complex_object));
+
+ test.done();
+};
+
+
+
+exports['JSONColumn deserializeValue values'] = function(test){
+ test.expect(10);
+ var column = new columns['json']({});
+
+ test.strictEqual(column.deserializeValue(null), null);
+ test.strictEqual(column.deserializeValue('null'), null);
+ test.strictEqual(column.deserializeValue('0'), 0);
+ test.strictEqual(column.deserializeValue('123'), 123);
+ test.strictEqual(column.deserializeValue('1.1'), 1.1);
+ test.strictEqual(column.deserializeValue('true'), true);
+ test.strictEqual(column.deserializeValue('false'), false);
+ test.strictEqual(column.deserializeValue('"1.1"'), '1.1');
+ test.strictEqual(column.deserializeValue('"value"'), 'value');
+ test.deepEqual(column.deserializeValue(JSON.stringify(complex_object)), complex_object);
+
+ test.done();
+};
+
+
+
+exports['JSONColumn defaultValue'] = function(test){
+ test.expect(3);
+ var column = new columns['json']({defaultValue: complex_object});
+
+ test.deepEqual(column.serializeValue(null), complex_object);
+ test.deepEqual(column.deserializeValue(null), complex_object);
+ test.deepEqual(column.deserializeValue('null'), null);
+
+ test.done();
+};

0 comments on commit 0554da5

Please sign in to comment.