Skip to content

Instantly share code, notes, and snippets.

@sr3d
Created March 26, 2011 06:13
Show Gist options
  • Save sr3d/888070 to your computer and use it in GitHub Desktop.
Save sr3d/888070 to your computer and use it in GitHub Desktop.
var TinyORM = (function(options) {
/* a hash containing all the available models */
var Models = {};
var connection = null;
var config = _.extend({
/* The name of the database which Ti will open. The local db is located at:
~/Library/Application Support/iPhone Simulator/4.2/Applications/APP_ID/Library/Application Support/database/dbname
*/
dbname: 'add.db',
/* Logging is on by default */
logging: true,
/* The id column is added automatically to the model. Default data type is INTEGER, but
also can be TEXT (e.g. MongoDB)
*/
modelIdDataType: 'INTEGER',
/* The created models can be attached to a scope. To make it availble globally,
Pass the global scope as "this". To make the models under a certain namespace, pass in the namespace instead
e.g. App.Models
*/
scope: null
}, options || {} );
var log = function(msg, force) {
if(config.logging || force) {
config.logger ? config.logger(msg) : Ti.API.debug(msg);
};
};
var connect = function() {
if( connection ) { return connection; };
connection = Ti.Database.open(config.dbname);
connection.apply = function(){}.apply;
log('ORM Connected to ' + config.dbname, true);
return connection;
};
var executeScalar = function(){
log('ORM ' + JSON.stringify(arguments));
var rows = connect().execute.apply(this, arguments);
var value = rows.isValidRow() ? rows.field(0) : null;
rows.close();
return value;
};
var executeNonSelect = function() {
try {
log('ORM executeNonSelect ' + JSON.stringify(arguments));
var row = connect().execute.apply(this, arguments);
row.close();
} catch(ex) {
log(ex, true);
alert(ex);
}
};
/* make sure the string is unescaped correctly, " => " and & => & */
function unescapeString(s){
return s && s.charCodeAt ? s.replace(/"/g, '"').replace(/&/g,'&') : s;
};
/* 2010-09-28T19:40:28-05:00 */
Date.prototype.toRailsISOString = function() {
function f(n){return n<10? '0' + n : n;};
var offset = -this.getTimezoneOffset();
offset = ( offset > 0 ? '+' : '-' ) + f(Math.abs(Math.floor(offset/60))) + ':' + f( offset % 60 );
return this.getFullYear()+'-'+
f(this.getMonth()+1) +'-'+
f(this.getDate())+ 'T'+
f(this.getHours())+ ':'+
f(this.getMinutes())+ ':'+
f(this.getSeconds()) + offset;
};
var Base = {};
Base.InstanceMethods = {
getAttributes: function() {
var attr = {};
var model = Models[this.className];
var self = this;
_.each(model.fields, function(v,k) {
attr[ k ] = self[ k ];
});
return attr;
}
,setAttributes: function( attrs ) {
_.extend(this, attrs);
return this;
}
,updateAttributes: function( attrs ) {
this.setAttributes(attrs);
this.save();
}
,save: function( skipValidation ) {
if( skipValidation === null ) { skipValidation = false; }
if( !skipValidation && !this.validate() ) { return false; };
var db = connect();
var sql;
var fields = Models[ this.className ].fields;
var self = this;
if( this.persisted ) {
if(fields.updated) { self.updated = (new Date()).toString(); };
sql = 'UPDATE '+ this.tableName + ' SET ';
var values = [], cols = [];
_.each(fields, function(v, k) {
cols.push( '"' + k + '" = ?' );
values.push( fields[k] == 'DATETIME' ? ( _.isDate( self[ k ] ) ? self[k].valueOf() : Date.parse(self[k]).valueOf() ) : (fields[k] == 'SERIALIZE' ? JSON.stringify(self[k]) : self[k]) );
});
sql += cols.join(', ') + ' WHERE id = ?'; // + this.id + '"';
values.push(this.id);
log(sql + ' ' + JSON.stringify(values) );
db.execute.apply(db, [sql].concat(values) );
} else {
sql = 'INSERT INTO ' + this.tableName;
var values = [], cols = [], placeHolders = [], self = this;
// add "created" magic column
if(fields.created) { self.created = (new Date()).toString(); };
_.each(fields, function(v, k) {
cols.push('"' + k + '"' );
values.push( fields[k] == 'DATETIME' ? ( _.isDate( self[ k ] ) ? self[k].valueOf() : Date.parse(self[k]).valueOf() ) : (fields[k] == 'SERIALIZE' ? JSON.stringify(self[k]) : self[k]) );
placeHolders.push('?');
} );
sql += ' (' + cols.join(',') + ') ';
sql += ' VALUES ( ' + placeHolders.join(',') +' )';
log(sql + '[' + JSON.stringify(values) + ']' );
db.execute.apply(db, [sql].concat(values));
this.id = db.lastInsertRowId;
this.persisted = true;
}
this.errors = [];
return true;
}
,destroy: function() {
var sql = 'DELETE FROM '+ this.tableName + ' WHERE id = ?';
log( sql + ' [' + this.id + ']');
connect().execute(sql, this.id);
}
,validate: function() {
this.errors = {};
var validators = Models[this.className].validators;
for(var i = 0; i < validators.length; i++ ) {
if( !validators[i](this) ) {
return false;
};
};
return true;
}
,toString: function() {
return JSON.stringify(this.getAttributes());
}
,toRailsParamsHash: function(baseName) {
if( !baseName ) { baseName = this.className.toLowerCase(); };
var hash = {};
var self = this;
_.each(Models[this.className].fields, function(v, k) {
// autoconvert date/ date in integer formats to Date object, then make sure it's in RailsISOString
hash[baseName + '[' + k +']' ] = ( v == 'DATETIME' ? ( _.isDate(self[k]) ? self[k].toRailsISOString() : (self[k] ? (new Date(self[k])).toRailsISOString() : null ) ) : self[k] );
});
return hash;
}
};
Base.ClassMethods = {
find: function(model, id, options) {
options = _.extend({orderBy: null}, options || {});
var result;
var db = connect(); // hack here
var sql;
var fields = _.keys(model.fields);
if( _.isNumber(id) || _.isString(id) ) {
sql = 'SELECT * FROM '+ model.tableName +' WHERE id = ?';
log('ORM ' + sql + '[' + id + ']');
var rs = db.execute(sql, id);
if (rs.isValidRow()) {
result = new model();
result.persisted = true;
for(var i = 0; i < fields.length; i++ ) {
result[ fields[i] ] = model.fields[fields[i]] == 'SERIALIZE' ? JSON.parse(rs.fieldByName( fields[i] )) : unescapeString(rs.fieldByName( fields[i] ) );
};
}
rs.close();
} else { // isArray or null
var rs;
if( id == null ) {
sql ='SELECT * FROM '+ model.tableName;
if(options.orderBy){ sql += ' ORDER BY ' + options.orderBy; };
log('ORM ' + sql);
rs = db.execute(sql);
} else { // find in specific id
sql = 'SELECT * FROM '+ model.tableName +' WHERE id IN (?)';
if(options.orderBy){ sql += ' ORDER BY ' + options.orderBy; };
log( 'ORM ' + sql + '[' + id + ']' );
rs = db.execute(sql, id);
};
result = [];
while (rs.isValidRow()) {
var obj = new model();
obj.persisted = true;
for( var i = 0; i < fields.length; i++ ) {
obj[ fields[i] ] = model.fields[fields[i]] == 'SERIALIZE' ? JSON.parse(rs.fieldByName( fields[i] )) : unescapeString(rs.fieldByName( fields[i] ) );
};
result.push(obj);
rs.next();
};
rs.close();
}
return result;
}
/* WIP */
,findBySQL: function( model, /* String or []*/ sql, options ) {
options = _.extend({
all: false
}, options || {} );
var db = connect();
var rs, result = [];
log('ORM ' + JSON.stringify(sql));
if(_.isString(sql)) {
rs = db.execute(sql);
} else {
rs = db.execute.apply(db, sql);
};
result = [];
while (rs.isValidRow()) {
var obj = new model();
obj.persisted = true;
for(var i = 0, fieldCount = rs.fieldCount(); i < fieldCount; i++ ) {
obj[ rs.fieldName(i) ] = (model.fields[ rs.fieldName(i) ] == 'SERIALIZE') ? JSON.parse( rs.field(i) ) : unescapeString(rs.field(i));
};
result.push(obj);
if(options.first) { break;};
rs.next();
};
rs.close();
log('ORM Found ' + result.length + ' records');
return options.first ? result[0] : result;
}
,create: function( model, json, skipValidation ) {
var obj = new model( json );
obj.save(skipValidation);
return obj;
}
,destroy: function(model, id) {
var sql = 'DELETE FROM '+ model.tableName + ' WHERE id = ?';
log( 'ORM ' + sql + ' [' + id + ']');
var rs = connect().execute(sql, id);
// rs.close();
}
,createTable: function(model) {
var sql = 'CREATE TABLE IF NOT EXISTS '+ model.tableName + ' (id ' + config.modelIdDataType +' PRIMARY KEY';
var cols = [];
_.each(model.fields, function(v,k) {
cols.push( '"' + k + '" ' + (v == 'DATETIME' ? 'FLOAT' : (v == 'SERIALIZE' ? 'TEXT' : v)) );
});
sql += ', ' + cols.join(', ') + ')';
log('ORM ' + sql);
connect().execute(sql);
}
,truncate: function(model) {
var sql = 'DELETE FROM ' + model.tableName;
log('ORM ' + sql);
connect().execute(sql);
}
,addValidator: function( model, validator ) {
model.validators.push(validator);
}
,validatesPresenceOf: function(model, field, message) {
model.addValidator(function(instance) {
var value = (instance.getAttributes())[field];
if(!value || /^\s*$/.test(value)) {
instance.errors[ field ] = message || (field + ' ' + ' is required');
return false;
};
return true;
});
}
,validatesFormatOf: function(model, field, format, message) {
model.addValidator(function(instance) {
var value = instance.getAttributes()[field];
if(value && format.test(value)) { return true; };
instance.errors[ field ] = message || ('Wrong format for ' + field);
return false;
});
}
,validatesNumericalityOf: function(model, field, message) {
model.addValidator(function(instance) {
var value = instance.getAttributes()[field];
if(_.isNumber(+value)) { return true; };
instance.errors[ field ] = message || ( field + ' must be a number');
return false;
});
}
};
Base.createModel = function(modelName, tableName, fields, instanceMethods, classMethods, options) {
options = _.extend({
createTable: true
}, options || {});
var model = function( /* string or json object */ json, options) {
if( json ) {
if( _.isString(json) ) {
json = JSON.parse(json);
};
_.extend( this, json );
};
this.className = modelName;
this.tableName = tableName;
this.persisted = false;
this.errors = [];
};
Models[ modelName ] = model;
_.extend(model.prototype, Base.InstanceMethods);
_.extend(model.prototype, instanceMethods || {} );
_.each( _.extend({
tableName: tableName,
className: modelName,
validators: [],
fields: fields
}, Base.ClassMethods), function(v,k) {
var value = v;
// attach the class func to the model, appending the model as the first arg
if( _.isFunction(v) ) {
value = _.bind(v, model, model);
};
model[k] = value;
});
_.extend(model, classMethods);
if( true || !config.migrated) { model.createTable(); };
// add the id column to the field list
model.fields.id = config.modelIdDataType;
// alias so we can have access to the Model directly under the scope
if(config.scope) { config.scope[ modelName ] = model;}
log('ORM ' + modelName + ' Model initialized');
return model;
};
return {
config: config,
connection: connection,
connect: connect,
log: log,
Models: Models,
Base: Base,
executeScalar: executeScalar,
executeNonSelect: executeNonSelect
};
})({scope: this, logger: log, modelIdDataType: 'TEXT' });
TinyORM.connect();
TinyORM.config.migrated = DB.get('migrated', false, 'app');
Ti.include(
'/app/models/snippet.js'
);
DB.set('migrate', true, 'app');
(function() {
TinyORM.Base.createModel('Snippet', 'snippets', {
title: 'TEXT',
description: 'TEXT',
favicon: 'TEXT',
url: 'TEXT',
created_at: 'DATETIME'
}, {
// InstanceMethods
}, {
// ClassMethods
refreshLocal: function(options) {
options = _.extend({ onData: function(){} }, options || {} );
}
});
Snippet.validatesPresenceOf('description','Description is required');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment