Created
March 26, 2011 06:13
-
-
Save sr3d/888070 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
TinyORM.connect(); | |
TinyORM.config.migrated = DB.get('migrated', false, 'app'); | |
Ti.include( | |
'/app/models/snippet.js' | |
); | |
DB.set('migrate', true, 'app'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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