├── .travis.yml ├── .gitignore ├── src ├── activerecord │ ├── middleware │ │ ├── noop.coffee │ │ ├── sql.coffee │ │ └── redis.coffee │ ├── configuration.coffee │ ├── plugin.coffee │ ├── adapters │ │ ├── rest.coffee │ │ ├── noop.coffee │ │ ├── redis.coffee │ │ ├── mysql.coffee │ │ └── sqlite.coffee │ ├── plugins │ │ ├── json.coffee │ │ └── logger.coffee │ └── model.coffee └── index.js ├── lib ├── index.js └── activerecord │ ├── middleware │ ├── noop.js │ ├── sql.js │ └── redis.js │ ├── configuration.js │ ├── adapters │ ├── rest.js │ ├── noop.js │ ├── redis.js │ ├── mysql.js │ └── sqlite.js │ ├── plugin.js │ ├── plugins │ ├── logger.js │ └── json.js │ └── model.js ├── examples ├── rest.coffee ├── config.coffee ├── redis.coffee ├── mysql-query.coffee ├── redis-middleware.coffee ├── query.coffee ├── user.coffee ├── relations.coffee └── plugins.coffee ├── package.json ├── Cakefile ├── test └── model.coffee └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | examples/test.db 4 | -------------------------------------------------------------------------------- /src/activerecord/middleware/noop.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class NoopMiddleware 2 | @supports: 3 | beforeWrite: false 4 | afterWrite: false -------------------------------------------------------------------------------- /src/activerecord/configuration.coffee: -------------------------------------------------------------------------------- 1 | exports.Configuration = class Configuration 2 | constructor: (@config) -> 3 | get: (adapter) -> @config[adapter] -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Configuration: require(__dirname + "/activerecord/configuration").Configuration, 3 | Model: require(__dirname + "/activerecord/model").Model, 4 | Plugin: require(__dirname + "/activerecord/plugin").Plugin 5 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('coffee-script'); 2 | 3 | module.exports = { 4 | Configuration: require(__dirname + "/activerecord/configuration").Configuration, 5 | Model: require(__dirname + "/activerecord/model").Model, 6 | Plugin: require(__dirname + "/activerecord/plugin").Plugin 7 | }; -------------------------------------------------------------------------------- /src/activerecord/middleware/sql.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class SQLMiddleware 2 | @supports: 3 | beforeWrite: false 4 | afterWrite: true 5 | 6 | constructor: (config) -> 7 | afterWrite: (options, results, cb) -> 8 | if results 9 | cb(null, results.lastID) 10 | else 11 | cb(true, null) -------------------------------------------------------------------------------- /examples/rest.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | adapters: ['rest'] 7 | fields: ['id', 'username', 'name', 'location', 'bio'] 8 | 9 | 10 | User.find 'meltingice', username: 1, (err, user) -> 11 | console.log user.toJSON() -------------------------------------------------------------------------------- /examples/config.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | 3 | module.exports = new ActiveRecord.Configuration 4 | sqlite: 5 | database: "#{__dirname}/test.db" 6 | mysql: 7 | host: 'localhost' 8 | database: 'test' 9 | user: 'test' 10 | password: 'password' 11 | redis: 12 | host: null 13 | port: null 14 | rest: 15 | url: 'https://api.heello.com' 16 | version: 1 17 | -------------------------------------------------------------------------------- /lib/activerecord/middleware/noop.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var NoopMiddleware; 3 | 4 | module.exports = NoopMiddleware = (function() { 5 | 6 | NoopMiddleware.name = 'NoopMiddleware'; 7 | 8 | function NoopMiddleware() {} 9 | 10 | NoopMiddleware.supports = { 11 | beforeWrite: false, 12 | afterWrite: false 13 | }; 14 | 15 | return NoopMiddleware; 16 | 17 | })(); 18 | 19 | }).call(this); 20 | -------------------------------------------------------------------------------- /lib/activerecord/configuration.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Configuration; 3 | 4 | exports.Configuration = Configuration = (function() { 5 | 6 | Configuration.name = 'Configuration'; 7 | 8 | function Configuration(config) { 9 | this.config = config; 10 | } 11 | 12 | Configuration.prototype.get = function(adapter) { 13 | return this.config[adapter]; 14 | }; 15 | 16 | return Configuration; 17 | 18 | })(); 19 | 20 | }).call(this); 21 | -------------------------------------------------------------------------------- /examples/redis.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | 7 | adapters: ['redis'] 8 | idMiddleware: ['redis'] 9 | idMiddlewareOptions: 10 | key: 'users:id' 11 | 12 | fields: ['id', 'username', 'name'] 13 | 14 | 15 | start = (new Date()).getTime() 16 | User.find 1, (err, user) -> 17 | end = (new Date()).getTime() 18 | 19 | console.log "retrieved in #{end - start}ms" -------------------------------------------------------------------------------- /examples/mysql-query.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | fields: ['id', 'username', 'name'] 7 | adapters: ['mysql'] 8 | 9 | isValid: -> 10 | return false if @username?.length is 0 or @name?.length is 0 11 | return true 12 | 13 | 14 | User.find 1, (err, user) -> 15 | user.username = "foobarfoo" 16 | user.save (err) -> 17 | console.log err if err 18 | console.log user.toJSON() -------------------------------------------------------------------------------- /src/activerecord/plugin.coffee: -------------------------------------------------------------------------------- 1 | # Allows you to hook into many of the various steps of the 2 | # ActiveRecord model. 3 | exports.Plugin = class Plugin 4 | # The plugin is given 5 | constructor: (@model) -> 6 | 7 | isValid: -> true 8 | 9 | beforeInit: -> true 10 | afterInit: -> true 11 | 12 | afterFind: -> true 13 | 14 | beforeSave: -> true 15 | afterSave: -> true 16 | 17 | beforeCreate: -> true 18 | afterCreate: -> true 19 | 20 | beforeUpdate: -> true 21 | afterUpdate: -> true 22 | 23 | beforeDelete: -> true 24 | afterDelete: -> true -------------------------------------------------------------------------------- /lib/activerecord/middleware/sql.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var SQLMiddleware; 3 | 4 | module.exports = SQLMiddleware = (function() { 5 | 6 | SQLMiddleware.name = 'SQLMiddleware'; 7 | 8 | SQLMiddleware.supports = { 9 | beforeWrite: false, 10 | afterWrite: true 11 | }; 12 | 13 | function SQLMiddleware(config) {} 14 | 15 | SQLMiddleware.prototype.afterWrite = function(options, results, cb) { 16 | if (results) { 17 | return cb(null, results.lastID); 18 | } else { 19 | return cb(true, null); 20 | } 21 | }; 22 | 23 | return SQLMiddleware; 24 | 25 | })(); 26 | 27 | }).call(this); 28 | -------------------------------------------------------------------------------- /src/activerecord/middleware/redis.coffee: -------------------------------------------------------------------------------- 1 | redis = require 'redis' 2 | 3 | module.exports = class RedisMiddleware 4 | @supports: 5 | beforeWrite: true 6 | afterWrite: false 7 | 8 | defaultOptions: 9 | host: null 10 | port: null 11 | 12 | constructor: (config) -> 13 | opts = @getOptions(config) 14 | @client = redis.createClient opts.port, opts.host 15 | 16 | beforeWrite: (opts, cb) -> 17 | @client.incr opts.key, (err, res) -> cb(err, res) 18 | 19 | getOptions: (opts) -> 20 | options = {} 21 | for own key, val of @defaultOptions 22 | if opts[key]? 23 | options[key] = opts[key] 24 | else 25 | options[key] = val 26 | 27 | options -------------------------------------------------------------------------------- /examples/redis-middleware.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | idMiddleware: 'redis' 7 | idMiddlewareOptions: 8 | key: 'user:id' 9 | 10 | fields: ['id', 'username', 'name'] 11 | 12 | sqlite3 = require('sqlite3').verbose() 13 | db = new sqlite3.Database "#{__dirname}/test.db" 14 | db.serialize -> 15 | db.run "CREATE TABLE IF NOT EXISTS users (id INTEGER, username VARCHAR(20), name VARCHAR(255))", [], (err) -> 16 | console.log err if err 17 | 18 | user = new User name: 'Ryan', username: 'meltingice' 19 | user.save (err) -> 20 | return console.log err if err 21 | console.log user.toJSON() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "activerecord", 3 | "description": "An ORM that supports multiple database systems (SQL/NoSQL) and ID generation middleware.", 4 | "version": "0.2.1", 5 | "author": "Ryan LeFevre (http://meltingice.net)", 6 | "homepage": "https://github.com/meltingice/node-activerecord", 7 | "main": "./lib", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/meltingice/node-activerecord.git" 11 | }, 12 | "devDependencies": { 13 | "coffee-script": ">= 1.3.1", 14 | "sqlite3": ">= 2.1.3", 15 | "glob": ">= 3.1.9", 16 | "mocha": ">= 1.0.3", 17 | "should": ">= 0.6.3" 18 | }, 19 | "scripts": { 20 | "test": "mocha --compilers coffee:coffee-script --require should --reporter spec" 21 | } 22 | } -------------------------------------------------------------------------------- /src/activerecord/adapters/rest.coffee: -------------------------------------------------------------------------------- 1 | rest = require 'restler' 2 | 3 | module.exports = class RestAdapter 4 | constructor: (@config) -> 5 | 6 | read: (action, endpoint, params, opts, cb) -> 7 | url = @buildUrl action, endpoint 8 | 9 | if params.length is 1 10 | params = params[0] 11 | 12 | rest.get(url, query: params).on 'complete', (data, resp) -> 13 | data = [data] unless Array.isArray data 14 | cb(null, data) 15 | 16 | # Write/delete operations are noop right now since they will require 17 | # authentication. 18 | write: (id, table, data, newRecord, opts, cb) -> cb(null, null) 19 | delete: (id, table, opts, cb) -> cb(null, null) 20 | 21 | buildUrl: (action, endpoint) -> 22 | "#{@config.url}/#{@config.version}/#{endpoint}/#{action}.json" 23 | -------------------------------------------------------------------------------- /src/activerecord/adapters/noop.coffee: -------------------------------------------------------------------------------- 1 | # No operation adapter - useful for testing or if you want to disable 2 | # data storage for a model. 3 | module.exports = class NoopAdapter 4 | @resultSet: [] 5 | @callStack: [] 6 | 7 | read: (args...) -> 8 | args.unshift "read" 9 | NoopAdapter.callStack.push args 10 | 11 | if typeof args[args.length - 1] is "function" 12 | args.pop()(NoopAdapter.resultSet) 13 | 14 | write: (args...) -> 15 | args.unshift "write" 16 | NoopAdapter.callStack.push args 17 | 18 | if typeof args[args.length - 1] is "function" 19 | args.pop()(true) 20 | 21 | delete: (args...) -> 22 | args.unshift "delete" 23 | NoopAdapter.callStack.push args 24 | 25 | if typeof args[args.length - 1] is "function" 26 | args.pop()(true) -------------------------------------------------------------------------------- /examples/query.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | fields: ['id', 'username', 'name'] 7 | 8 | isValid: -> 9 | return false if @username?.length is 0 or @name?.length is 0 10 | return true 11 | 12 | 13 | sqlite3 = require('sqlite3').verbose() 14 | db = new sqlite3.Database "#{__dirname}/test.db" 15 | db.serialize -> 16 | db.run "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(20), name VARCHAR(255))", [], (err) -> 17 | console.log err if err 18 | 19 | user = new User name: 'Ryan', username: 'meltingice' 20 | user.save (err) -> 21 | unless err 22 | User.find "SELECT * FROM users WHERE id = ?", 1, (err, user) -> 23 | console.log user.toJSON() 24 | db.run "DROP TABLE users" -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | util = require 'util' 3 | coffee = require 'coffee-script' 4 | glob = require 'glob' 5 | {spawn} = require 'child_process' 6 | 7 | search = "**/*.coffee" 8 | inputDir = "src" 9 | outputDir = "lib" 10 | 11 | task 'watch', 'Automatically recompile whenever the source changes', -> 12 | util.log "Watching for source changes" 13 | 14 | glob "#{inputDir}/#{search}", (er, files) -> 15 | for file in files then do (file) -> 16 | fs.watchFile file, (curr, prev) -> 17 | if +curr.mtime isnt +prev.mtime 18 | util.log "#{file} updated" 19 | invoke 'compile' 20 | 21 | task 'compile', 'Compile the Coffeescript source to JS', -> 22 | glob "#{inputDir}/#{search}", (er, files) -> 23 | for file in files 24 | inCode = fs.readFileSync file, "utf8" 25 | outCode = coffee.compile inCode 26 | outFile = "#{outputDir}/" + file.substr("#{inputDir}/".length).replace 'coffee', 'js' 27 | 28 | fs.writeFileSync outFile, outCode 29 | 30 | util.log "Compile: #{file} -> #{outFile}" 31 | -------------------------------------------------------------------------------- /examples/user.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | fields: ['id', 'username', 'name'] 7 | plugins: -> [ 8 | 'json' 9 | 'logger' 10 | ] 11 | 12 | filterUsername: (username) -> username + " bob" 13 | isValid: -> 14 | return false if @username?.length is 0 or @name?.length is 0 15 | return true 16 | 17 | 18 | sqlite3 = require('sqlite3').verbose() 19 | db = new sqlite3.Database "#{__dirname}/test.db" 20 | db.serialize -> 21 | db.run "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(20), name VARCHAR(255))", [], (err) -> 22 | console.log err if err 23 | 24 | user = new User name: 'Ryan', username: 'meltingice' 25 | user.save (err) -> 26 | unless err 27 | User.find 1, (err, user) -> 28 | console.log user.toJSON() 29 | user.name = "Bob" 30 | user.save (err) -> 31 | console.log user.toJSON() 32 | 33 | user.delete (err) -> console.log user.toJSON(); db.run "DROP TABLE users" -------------------------------------------------------------------------------- /lib/activerecord/adapters/rest.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var RestAdapter, rest; 3 | 4 | rest = require('restler'); 5 | 6 | module.exports = RestAdapter = (function() { 7 | 8 | RestAdapter.name = 'RestAdapter'; 9 | 10 | function RestAdapter(config) { 11 | this.config = config; 12 | } 13 | 14 | RestAdapter.prototype.read = function(action, endpoint, params, opts, cb) { 15 | var url; 16 | url = this.buildUrl(action, endpoint); 17 | if (params.length === 1) { 18 | params = params[0]; 19 | } 20 | return rest.get(url, { 21 | query: params 22 | }).on('complete', function(data, resp) { 23 | if (!Array.isArray(data)) { 24 | data = [data]; 25 | } 26 | return cb(null, data); 27 | }); 28 | }; 29 | 30 | RestAdapter.prototype.write = function(id, table, data, newRecord, opts, cb) { 31 | return cb(null, null); 32 | }; 33 | 34 | RestAdapter.prototype["delete"] = function(id, table, opts, cb) { 35 | return cb(null, null); 36 | }; 37 | 38 | RestAdapter.prototype.buildUrl = function(action, endpoint) { 39 | return "" + this.config.url + "/" + this.config.version + "/" + endpoint + "/" + action + ".json"; 40 | }; 41 | 42 | return RestAdapter; 43 | 44 | })(); 45 | 46 | }).call(this); 47 | -------------------------------------------------------------------------------- /src/activerecord/plugins/json.coffee: -------------------------------------------------------------------------------- 1 | {Plugin} = require __dirname + "/../plugin" 2 | 3 | module.exports = class json extends Plugin 4 | toJSON: (incRelations = false, cb = ->) -> 5 | return {} unless @isLoaded() 6 | 7 | pretty = {} 8 | 9 | for field in @fields 10 | if @fieldsProtected? 11 | continue if field in @fieldsProtected 12 | 13 | pretty[field] = @[field] 14 | 15 | if incRelations 16 | queryWait = {} 17 | for type in ['hasOne', 'belongsTo', 'hasMany'] 18 | for association in @[type]() 19 | if Array.isArray association 20 | association = association[0] 21 | 22 | assocName = association.toAssociationName(type is 'hasMany') 23 | queryWait[assocName] = true 24 | 25 | do (type, assocName) => 26 | @[assocName] (err, assoc) => 27 | queryWait[assocName] = false 28 | return if err 29 | 30 | if type is 'hasMany' 31 | pretty[assocName] = [] 32 | pretty[assocName].push m.toJSON() for m in assoc 33 | else 34 | pretty[assocName] = assoc.toJSON() 35 | 36 | for own key, val of queryWait 37 | return if val is true 38 | 39 | cb(pretty) 40 | 41 | else 42 | pretty 43 | 44 | -------------------------------------------------------------------------------- /src/activerecord/adapters/redis.coffee: -------------------------------------------------------------------------------- 1 | redis = require 'redis' 2 | 3 | module.exports = class RedisAdapter 4 | defaultOptions: 5 | host: null 6 | port: null 7 | 8 | constructor: (config) -> 9 | opts = @getOptions(config) 10 | @client = redis.createClient opts.port, opts.host 11 | 12 | read: (finder, namespace, params, opts, cb) -> 13 | finder = [finder] unless Array.isArray finder 14 | multi = @client.multi() 15 | multi.hgetall "#{namespace}:#{f}" for f in finder 16 | multi.exec (err, replies) => 17 | return cb(err, []) if err 18 | 19 | # Redis returns all values as Strings, so we force-parse the 20 | # primary index as an int. 21 | result = [] 22 | for r in replies 23 | continue if r is null 24 | r[opts.primaryIndex] = parseInt(r[opts.primaryIndex], 10) 25 | result.push r 26 | 27 | cb(null, result) 28 | 29 | write: (id, namespace, data, newRecord, opts, cb) -> 30 | @client.hmset "#{namespace}:#{id}", data, (err) => cb(err, null) 31 | 32 | delete: (id, namespace, opts, cb) -> 33 | @client.del "#{namespace}:#{id}", (err) => cb(err, null) 34 | 35 | getOptions: (opts) -> 36 | options = {} 37 | for own key, val of @defaultOptions 38 | if opts[key]? 39 | options[key] = opts[key] 40 | else 41 | options[key] = val 42 | 43 | options -------------------------------------------------------------------------------- /lib/activerecord/plugin.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Plugin; 3 | 4 | exports.Plugin = Plugin = (function() { 5 | 6 | Plugin.name = 'Plugin'; 7 | 8 | function Plugin(model) { 9 | this.model = model; 10 | } 11 | 12 | Plugin.prototype.isValid = function() { 13 | return true; 14 | }; 15 | 16 | Plugin.prototype.beforeInit = function() { 17 | return true; 18 | }; 19 | 20 | Plugin.prototype.afterInit = function() { 21 | return true; 22 | }; 23 | 24 | Plugin.prototype.afterFind = function() { 25 | return true; 26 | }; 27 | 28 | Plugin.prototype.beforeSave = function() { 29 | return true; 30 | }; 31 | 32 | Plugin.prototype.afterSave = function() { 33 | return true; 34 | }; 35 | 36 | Plugin.prototype.beforeCreate = function() { 37 | return true; 38 | }; 39 | 40 | Plugin.prototype.afterCreate = function() { 41 | return true; 42 | }; 43 | 44 | Plugin.prototype.beforeUpdate = function() { 45 | return true; 46 | }; 47 | 48 | Plugin.prototype.afterUpdate = function() { 49 | return true; 50 | }; 51 | 52 | Plugin.prototype.beforeDelete = function() { 53 | return true; 54 | }; 55 | 56 | Plugin.prototype.afterDelete = function() { 57 | return true; 58 | }; 59 | 60 | return Plugin; 61 | 62 | })(); 63 | 64 | }).call(this); 65 | -------------------------------------------------------------------------------- /examples/relations.coffee: -------------------------------------------------------------------------------- 1 | ActiveRecord = require '../lib' 2 | config = require __dirname + "/config" 3 | 4 | class User extends ActiveRecord.Model 5 | config: config 6 | fields: ['id', 'username', 'name'] 7 | hasMany: -> [ 8 | Message 9 | ] 10 | 11 | loadMessages: (cb) -> 12 | Message.findAll "SELECT * FROM messages WHERE user_id = ?", @id, cb 13 | 14 | class Message extends ActiveRecord.Model 15 | config: config 16 | fields: ['id', 'user_id', 'text'] 17 | belongsTo: -> [ 18 | User 19 | ] 20 | 21 | sqlite3 = require('sqlite3') 22 | db = new sqlite3.Database "#{__dirname}/test.db" 23 | db.serialize -> 24 | db.run "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, `text` TEXT)", [] 25 | db.run "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(20), name VARCHAR(255))", [], (err) -> 26 | console.log err if err 27 | 28 | user = new User name: 'Ryan', username: 'meltingice' 29 | user.save (err) -> 30 | message = new Message user_id: user.id, text: "This is a test!" 31 | message.save (err) -> 32 | message = new Message user_id: user.id, text: "Also a test!" 33 | message.save (err) -> 34 | user.messages (err, messages) -> 35 | console.log message.toJSON() for message in messages 36 | 37 | db.run "DROP TABLE messages" 38 | db.run "DROP TABLE users" -------------------------------------------------------------------------------- /lib/activerecord/middleware/redis.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var RedisMiddleware, redis, 3 | __hasProp = {}.hasOwnProperty; 4 | 5 | redis = require('redis'); 6 | 7 | module.exports = RedisMiddleware = (function() { 8 | 9 | RedisMiddleware.name = 'RedisMiddleware'; 10 | 11 | RedisMiddleware.supports = { 12 | beforeWrite: true, 13 | afterWrite: false 14 | }; 15 | 16 | RedisMiddleware.prototype.defaultOptions = { 17 | host: null, 18 | port: null 19 | }; 20 | 21 | function RedisMiddleware(config) { 22 | var opts; 23 | opts = this.getOptions(config); 24 | this.client = redis.createClient(opts.port, opts.host); 25 | } 26 | 27 | RedisMiddleware.prototype.beforeWrite = function(opts, cb) { 28 | return this.client.incr(opts.key, function(err, res) { 29 | return cb(err, res); 30 | }); 31 | }; 32 | 33 | RedisMiddleware.prototype.getOptions = function(opts) { 34 | var key, options, val, _ref; 35 | options = {}; 36 | _ref = this.defaultOptions; 37 | for (key in _ref) { 38 | if (!__hasProp.call(_ref, key)) continue; 39 | val = _ref[key]; 40 | if (opts[key] != null) { 41 | options[key] = opts[key]; 42 | } else { 43 | options[key] = val; 44 | } 45 | } 46 | return options; 47 | }; 48 | 49 | return RedisMiddleware; 50 | 51 | })(); 52 | 53 | }).call(this); 54 | -------------------------------------------------------------------------------- /lib/activerecord/adapters/noop.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var NoopAdapter, 3 | __slice = [].slice; 4 | 5 | module.exports = NoopAdapter = (function() { 6 | 7 | NoopAdapter.name = 'NoopAdapter'; 8 | 9 | function NoopAdapter() {} 10 | 11 | NoopAdapter.resultSet = []; 12 | 13 | NoopAdapter.callStack = []; 14 | 15 | NoopAdapter.prototype.read = function() { 16 | var args; 17 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 18 | args.unshift("read"); 19 | NoopAdapter.callStack.push(args); 20 | if (typeof args[args.length - 1] === "function") { 21 | return args.pop()(NoopAdapter.resultSet); 22 | } 23 | }; 24 | 25 | NoopAdapter.prototype.write = function() { 26 | var args; 27 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 28 | args.unshift("write"); 29 | NoopAdapter.callStack.push(args); 30 | if (typeof args[args.length - 1] === "function") { 31 | return args.pop()(true); 32 | } 33 | }; 34 | 35 | NoopAdapter.prototype["delete"] = function() { 36 | var args; 37 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 38 | args.unshift("delete"); 39 | NoopAdapter.callStack.push(args); 40 | if (typeof args[args.length - 1] === "function") { 41 | return args.pop()(true); 42 | } 43 | }; 44 | 45 | return NoopAdapter; 46 | 47 | })(); 48 | 49 | }).call(this); 50 | -------------------------------------------------------------------------------- /examples/plugins.coffee: -------------------------------------------------------------------------------- 1 | # NOTE: the JSON plugin is included by default 2 | 3 | ActiveRecord = require '../lib' 4 | config = require __dirname + "/config" 5 | 6 | class User extends ActiveRecord.Model 7 | config: config 8 | fields: ['id', 'username', 'name', 'password'] 9 | fieldsProtected: ['password'] 10 | hasMany: -> [ 11 | Message 12 | ] 13 | 14 | loadMessages: (cb) -> 15 | Message.findAll "SELECT * FROM messages WHERE user_id = ?", @id, cb 16 | 17 | class Message extends ActiveRecord.Model 18 | config: config 19 | fields: ['id', 'user_id', 'text'] 20 | belongsTo: -> [ 21 | User 22 | ] 23 | 24 | sqlite3 = require('sqlite3') 25 | db = new sqlite3.Database "#{__dirname}/test.db" 26 | db.serialize -> 27 | db.run "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, `text` TEXT)", [] 28 | db.run "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(20), name VARCHAR(255), password VARCHAR(120))", [], (err) -> 29 | console.log err if err 30 | 31 | user = new User name: 'Ryan', username: 'meltingice' 32 | user.save (err) -> 33 | message = new Message user_id: user.id, text: "This is a test!" 34 | message.save (err) -> 35 | message = new Message user_id: user.id, text: "Also a test!" 36 | message.save (err) -> 37 | user.messages (err, messages) -> 38 | user.toJSON true, (pretty) -> 39 | console.log pretty 40 | 41 | db.run "DROP TABLE messages" 42 | db.run "DROP TABLE users" -------------------------------------------------------------------------------- /src/activerecord/plugins/logger.coffee: -------------------------------------------------------------------------------- 1 | {Plugin} = require __dirname + "/../plugin" 2 | util = require 'util' 3 | 4 | module.exports = class Logger extends Plugin 5 | beforeInit: -> 6 | util.log "Beginning initialization for #{@model.__proto__.constructor.name}" 7 | true 8 | 9 | afterInit: -> 10 | util.log "Finished initialization for #{@model.__proto__.constructor.name}" 11 | true 12 | 13 | afterFind: -> 14 | util.log "Found #{@model.__proto__.constructor.name} (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 15 | true 16 | 17 | beforeSave: -> 18 | util.log "Preparing to save #{@model.__proto__.constructor.name} (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 19 | true 20 | 21 | afterSave: -> 22 | util.log "Finished saving #{@model.__proto__.constructor.name} (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 23 | true 24 | 25 | beforeCreate: -> 26 | util.log "Creating new #{@model.__proto__.constructor.name}..." 27 | true 28 | 29 | afterCreate: -> 30 | util.log "New #{@model.__proto__.constructor.name} created (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 31 | true 32 | 33 | beforeUpdate: -> 34 | util.log "Updating #{@model.__proto__.constructor.name} (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 35 | true 36 | 37 | afterUpdate: -> 38 | util.log "Updated #{@model.__proto__.constructor.name} (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 39 | true 40 | 41 | beforeDelete: -> 42 | util.log "Preparing to delete #{@model.__proto__.constructor.name} (#{@model.primaryIndex} = #{@model[@model.primaryIndex]})" 43 | true 44 | 45 | afterDelete: -> 46 | util.log "Finished deleting #{@model.__proto__.constructor.name}" 47 | true -------------------------------------------------------------------------------- /src/activerecord/adapters/mysql.coffee: -------------------------------------------------------------------------------- 1 | mysql = require 'mysql' 2 | 3 | module.exports = class MySQLAdapter 4 | defaultOptions: 5 | primaryIndex: 'id' 6 | 7 | constructor: (@config) -> 8 | @db = mysql.createClient @config 9 | 10 | read: (finder, table, params, opts, cb) -> 11 | options = @getOptions(opts) 12 | 13 | if typeof finder is "string" and finder.length >= @MIN_SQL_SIZE 14 | sqlClause = finder 15 | else if Array.isArray finder 16 | sqlClause = "SELECT * FROM `#{table}` WHERE `#{options.primaryIndex}` IN (#{finder.join(',')})" 17 | else 18 | sqlClause = "SELECT * FROM `#{table}` WHERE `#{options.primaryIndex}` = ? LIMIT 1" 19 | params = [finder] 20 | 21 | @db.query sqlClause, params, (err, rows, fields) -> 22 | if err 23 | cb(err, []) 24 | else 25 | cb(null, rows) 26 | 27 | write: (id, table, data, newRecord, opts, cb) -> 28 | options = @getOptions(opts) 29 | 30 | params = [] 31 | params.push val for own key, val of data 32 | 33 | if newRecord is true 34 | sqlClause = @insertSql(table, data) 35 | params.unshift null 36 | else 37 | sqlClause = @updateSql(id, table, data, options.primaryIndex) 38 | params.push id 39 | 40 | @db.query sqlClause, params, (err, info) -> 41 | if err 42 | cb(err) 43 | else 44 | cb null, lastID: info.insertId 45 | 46 | delete: (id, table, opts, cb) -> 47 | options = @getOptions(opts) 48 | 49 | sqlClause = "DELETE FROM `#{table}` WHERE `#{options.primaryIndex}` = ?" 50 | @db.query sqlClause, [id], (err, info) -> 51 | if err 52 | cb(err) 53 | else 54 | cb(null, info) 55 | 56 | insertSql: (table, data) -> 57 | columns = ['`id`'] 58 | columns.push "`#{c}`" for c, val of data 59 | 60 | values = [] 61 | values.push "?" for i in [0...columns.length] 62 | 63 | columns = columns.join ',' 64 | values = values.join ',' 65 | 66 | "INSERT INTO `#{table}` (#{columns}) VALUES (#{values})" 67 | 68 | updateSql: (id, table, data, primaryIndex) -> 69 | columns = [] 70 | columns.push "`#{c}`=?" for own c, val of data 71 | 72 | tuples = columns.join ',' 73 | 74 | "UPDATE `#{table}` SET #{tuples} WHERE `#{primaryIndex}` = ?" 75 | 76 | getOptions: (opts) -> 77 | options = {} 78 | for own key, val of @defaultOptions 79 | if opts[key]? 80 | options[key] = opts[key] 81 | else 82 | options[key] = val 83 | 84 | options -------------------------------------------------------------------------------- /lib/activerecord/adapters/redis.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var RedisAdapter, redis, 3 | __hasProp = {}.hasOwnProperty; 4 | 5 | redis = require('redis'); 6 | 7 | module.exports = RedisAdapter = (function() { 8 | 9 | RedisAdapter.name = 'RedisAdapter'; 10 | 11 | RedisAdapter.prototype.defaultOptions = { 12 | host: null, 13 | port: null 14 | }; 15 | 16 | function RedisAdapter(config) { 17 | var opts; 18 | opts = this.getOptions(config); 19 | this.client = redis.createClient(opts.port, opts.host); 20 | } 21 | 22 | RedisAdapter.prototype.read = function(finder, namespace, params, opts, cb) { 23 | var f, multi, _i, _len, 24 | _this = this; 25 | if (!Array.isArray(finder)) { 26 | finder = [finder]; 27 | } 28 | multi = this.client.multi(); 29 | for (_i = 0, _len = finder.length; _i < _len; _i++) { 30 | f = finder[_i]; 31 | multi.hgetall("" + namespace + ":" + f); 32 | } 33 | return multi.exec(function(err, replies) { 34 | var r, result, _j, _len1; 35 | if (err) { 36 | return cb(err, []); 37 | } 38 | result = []; 39 | for (_j = 0, _len1 = replies.length; _j < _len1; _j++) { 40 | r = replies[_j]; 41 | if (r === null) { 42 | continue; 43 | } 44 | r[opts.primaryIndex] = parseInt(r[opts.primaryIndex], 10); 45 | result.push(r); 46 | } 47 | return cb(null, result); 48 | }); 49 | }; 50 | 51 | RedisAdapter.prototype.write = function(id, namespace, data, newRecord, opts, cb) { 52 | var _this = this; 53 | return this.client.hmset("" + namespace + ":" + id, data, function(err) { 54 | return cb(err, null); 55 | }); 56 | }; 57 | 58 | RedisAdapter.prototype["delete"] = function(id, namespace, opts, cb) { 59 | var _this = this; 60 | return this.client.del("" + namespace + ":" + id, function(err) { 61 | return cb(err, null); 62 | }); 63 | }; 64 | 65 | RedisAdapter.prototype.getOptions = function(opts) { 66 | var key, options, val, _ref; 67 | options = {}; 68 | _ref = this.defaultOptions; 69 | for (key in _ref) { 70 | if (!__hasProp.call(_ref, key)) continue; 71 | val = _ref[key]; 72 | if (opts[key] != null) { 73 | options[key] = opts[key]; 74 | } else { 75 | options[key] = val; 76 | } 77 | } 78 | return options; 79 | }; 80 | 81 | return RedisAdapter; 82 | 83 | })(); 84 | 85 | }).call(this); 86 | -------------------------------------------------------------------------------- /src/activerecord/adapters/sqlite.coffee: -------------------------------------------------------------------------------- 1 | sqlite3 = require('sqlite3') 2 | 3 | module.exports = class SQLiteAdapter 4 | MIN_SQL_SIZE: 15 5 | defaultOptions: 6 | primaryIndex: 'id' 7 | 8 | constructor: (@config) -> 9 | @db = new sqlite3.Database @config.database 10 | 11 | read: (finder, table, params, opts, cb) -> 12 | options = @getOptions(opts) 13 | 14 | if typeof finder is "string" and finder.length >= @MIN_SQL_SIZE 15 | sqlClause = finder 16 | else if Array.isArray finder 17 | sqlClause = "SELECT * FROM `#{table}` WHERE `#{options.primaryIndex}` IN (#{finder.join(',')})" 18 | else 19 | sqlClause = "SELECT * FROM `#{table}` WHERE `#{options.primaryIndex}` = ? LIMIT 1" 20 | params = [finder] 21 | 22 | @db.serialize => 23 | @db.all sqlClause, params, (err, rows) -> 24 | if err 25 | cb(err, []) 26 | else 27 | cb(null, rows) 28 | 29 | write: (id, table, data, newRecord, opts, cb) -> 30 | options = @getOptions(opts) 31 | 32 | params = [] 33 | params.push val for own key, val of data 34 | 35 | if newRecord is true 36 | sqlClause = @insertSql(table, data) 37 | params.unshift null 38 | else 39 | sqlClause = @updateSql(id, table, data, options.primaryIndex) 40 | params.push id 41 | 42 | @db.serialize => 43 | @db.run sqlClause, params, (err, info...) -> 44 | if err 45 | cb(err) 46 | else 47 | cb(null, @) 48 | 49 | delete: (id, table, opts, cb) -> 50 | options = @getOptions(opts) 51 | 52 | sqlClause = "DELETE FROM `#{table}` WHERE `#{options.primaryIndex}` = ?" 53 | @db.serialize => 54 | @db.run sqlClause, id, (err, info...) -> 55 | if err 56 | cb(err) 57 | else 58 | cb(null, @) 59 | 60 | insertSql: (table, data) -> 61 | columns = ['`id`'] 62 | columns.push "`#{c}`" for c, val of data 63 | 64 | values = [] 65 | values.push "?" for i in [0...columns.length] 66 | 67 | columns = columns.join ',' 68 | values = values.join ',' 69 | 70 | "INSERT INTO `#{table}` (#{columns}) VALUES (#{values})" 71 | 72 | updateSql: (id, table, data, primaryIndex) -> 73 | columns = [] 74 | columns.push "`#{c}`=?" for own c, val of data 75 | 76 | tuples = columns.join ',' 77 | 78 | "UPDATE `#{table}` SET #{tuples} WHERE `#{primaryIndex}` = ?" 79 | 80 | getOptions: (opts) -> 81 | options = {} 82 | for own key, val of @defaultOptions 83 | if opts[key]? 84 | options[key] = opts[key] 85 | else 86 | options[key] = val 87 | 88 | options -------------------------------------------------------------------------------- /lib/activerecord/plugins/logger.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Logger, Plugin, util, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }; 5 | 6 | Plugin = require(__dirname + "/../plugin").Plugin; 7 | 8 | util = require('util'); 9 | 10 | module.exports = Logger = (function(_super) { 11 | 12 | __extends(Logger, _super); 13 | 14 | Logger.name = 'Logger'; 15 | 16 | function Logger() { 17 | return Logger.__super__.constructor.apply(this, arguments); 18 | } 19 | 20 | Logger.prototype.beforeInit = function() { 21 | util.log("Beginning initialization for " + this.model.__proto__.constructor.name); 22 | return true; 23 | }; 24 | 25 | Logger.prototype.afterInit = function() { 26 | util.log("Finished initialization for " + this.model.__proto__.constructor.name); 27 | return true; 28 | }; 29 | 30 | Logger.prototype.afterFind = function() { 31 | util.log("Found " + this.model.__proto__.constructor.name + " (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 32 | return true; 33 | }; 34 | 35 | Logger.prototype.beforeSave = function() { 36 | util.log("Preparing to save " + this.model.__proto__.constructor.name + " (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 37 | return true; 38 | }; 39 | 40 | Logger.prototype.afterSave = function() { 41 | util.log("Finished saving " + this.model.__proto__.constructor.name + " (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 42 | return true; 43 | }; 44 | 45 | Logger.prototype.beforeCreate = function() { 46 | util.log("Creating new " + this.model.__proto__.constructor.name + "..."); 47 | return true; 48 | }; 49 | 50 | Logger.prototype.afterCreate = function() { 51 | util.log("New " + this.model.__proto__.constructor.name + " created (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 52 | return true; 53 | }; 54 | 55 | Logger.prototype.beforeUpdate = function() { 56 | util.log("Updating " + this.model.__proto__.constructor.name + " (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 57 | return true; 58 | }; 59 | 60 | Logger.prototype.afterUpdate = function() { 61 | util.log("Updated " + this.model.__proto__.constructor.name + " (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 62 | return true; 63 | }; 64 | 65 | Logger.prototype.beforeDelete = function() { 66 | util.log("Preparing to delete " + this.model.__proto__.constructor.name + " (" + this.model.primaryIndex + " = " + this.model[this.model.primaryIndex] + ")"); 67 | return true; 68 | }; 69 | 70 | Logger.prototype.afterDelete = function() { 71 | util.log("Finished deleting " + this.model.__proto__.constructor.name); 72 | return true; 73 | }; 74 | 75 | return Logger; 76 | 77 | })(Plugin); 78 | 79 | }).call(this); 80 | -------------------------------------------------------------------------------- /lib/activerecord/plugins/json.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Plugin, json, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }, 5 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 6 | 7 | Plugin = require(__dirname + "/../plugin").Plugin; 8 | 9 | module.exports = json = (function(_super) { 10 | 11 | __extends(json, _super); 12 | 13 | json.name = 'json'; 14 | 15 | function json() { 16 | return json.__super__.constructor.apply(this, arguments); 17 | } 18 | 19 | json.prototype.toJSON = function(incRelations, cb) { 20 | var assocName, association, field, pretty, queryWait, type, _i, _j, _len, _len1, _ref, _ref1, _results; 21 | if (incRelations == null) { 22 | incRelations = false; 23 | } 24 | if (cb == null) { 25 | cb = function() {}; 26 | } 27 | if (!this.isLoaded()) { 28 | return {}; 29 | } 30 | pretty = {}; 31 | _ref = this.fields; 32 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 33 | field = _ref[_i]; 34 | if (this.fieldsProtected != null) { 35 | if (__indexOf.call(this.fieldsProtected, field) >= 0) { 36 | continue; 37 | } 38 | } 39 | pretty[field] = this[field]; 40 | } 41 | if (incRelations) { 42 | queryWait = {}; 43 | _ref1 = ['hasOne', 'belongsTo', 'hasMany']; 44 | _results = []; 45 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 46 | type = _ref1[_j]; 47 | _results.push((function() { 48 | var _k, _len2, _ref2, _results1, 49 | _this = this; 50 | _ref2 = this[type](); 51 | _results1 = []; 52 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 53 | association = _ref2[_k]; 54 | if (Array.isArray(association)) { 55 | association = association[0]; 56 | } 57 | assocName = association.toAssociationName(type === 'hasMany'); 58 | queryWait[assocName] = true; 59 | _results1.push((function(type, assocName) { 60 | return _this[assocName](function(err, assoc) { 61 | var key, m, val, _l, _len3; 62 | queryWait[assocName] = false; 63 | if (err) { 64 | return; 65 | } 66 | if (type === 'hasMany') { 67 | pretty[assocName] = []; 68 | for (_l = 0, _len3 = assoc.length; _l < _len3; _l++) { 69 | m = assoc[_l]; 70 | pretty[assocName].push(m.toJSON()); 71 | } 72 | } else { 73 | pretty[assocName] = assoc.toJSON(); 74 | } 75 | for (key in queryWait) { 76 | if (!__hasProp.call(queryWait, key)) continue; 77 | val = queryWait[key]; 78 | if (val === true) { 79 | return; 80 | } 81 | } 82 | return cb(pretty); 83 | }); 84 | })(type, assocName)); 85 | } 86 | return _results1; 87 | }).call(this)); 88 | } 89 | return _results; 90 | } else { 91 | return pretty; 92 | } 93 | }; 94 | 95 | return json; 96 | 97 | })(Plugin); 98 | 99 | }).call(this); 100 | -------------------------------------------------------------------------------- /lib/activerecord/adapters/mysql.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var MySQLAdapter, mysql, 3 | __hasProp = {}.hasOwnProperty; 4 | 5 | mysql = require('mysql'); 6 | 7 | module.exports = MySQLAdapter = (function() { 8 | 9 | MySQLAdapter.name = 'MySQLAdapter'; 10 | 11 | MySQLAdapter.prototype.defaultOptions = { 12 | primaryIndex: 'id' 13 | }; 14 | 15 | function MySQLAdapter(config) { 16 | this.config = config; 17 | this.db = mysql.createClient(this.config); 18 | } 19 | 20 | MySQLAdapter.prototype.read = function(finder, table, params, opts, cb) { 21 | var options, sqlClause; 22 | options = this.getOptions(opts); 23 | if (typeof finder === "string" && finder.length >= this.MIN_SQL_SIZE) { 24 | sqlClause = finder; 25 | } else if (Array.isArray(finder)) { 26 | sqlClause = "SELECT * FROM `" + table + "` WHERE `" + options.primaryIndex + "` IN (" + (finder.join(',')) + ")"; 27 | } else { 28 | sqlClause = "SELECT * FROM `" + table + "` WHERE `" + options.primaryIndex + "` = ? LIMIT 1"; 29 | params = [finder]; 30 | } 31 | return this.db.query(sqlClause, params, function(err, rows, fields) { 32 | if (err) { 33 | return cb(err, []); 34 | } else { 35 | return cb(null, rows); 36 | } 37 | }); 38 | }; 39 | 40 | MySQLAdapter.prototype.write = function(id, table, data, newRecord, opts, cb) { 41 | var key, options, params, sqlClause, val; 42 | options = this.getOptions(opts); 43 | params = []; 44 | for (key in data) { 45 | if (!__hasProp.call(data, key)) continue; 46 | val = data[key]; 47 | params.push(val); 48 | } 49 | if (newRecord === true) { 50 | sqlClause = this.insertSql(table, data); 51 | params.unshift(null); 52 | } else { 53 | sqlClause = this.updateSql(id, table, data, options.primaryIndex); 54 | params.push(id); 55 | } 56 | return this.db.query(sqlClause, params, function(err, info) { 57 | if (err) { 58 | return cb(err); 59 | } else { 60 | return cb(null, { 61 | lastID: info.insertId 62 | }); 63 | } 64 | }); 65 | }; 66 | 67 | MySQLAdapter.prototype["delete"] = function(id, table, opts, cb) { 68 | var options, sqlClause; 69 | options = this.getOptions(opts); 70 | sqlClause = "DELETE FROM `" + table + "` WHERE `" + options.primaryIndex + "` = ?"; 71 | return this.db.query(sqlClause, [id], function(err, info) { 72 | if (err) { 73 | return cb(err); 74 | } else { 75 | return cb(null, info); 76 | } 77 | }); 78 | }; 79 | 80 | MySQLAdapter.prototype.insertSql = function(table, data) { 81 | var c, columns, i, val, values, _i, _ref; 82 | columns = ['`id`']; 83 | for (c in data) { 84 | val = data[c]; 85 | columns.push("`" + c + "`"); 86 | } 87 | values = []; 88 | for (i = _i = 0, _ref = columns.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 89 | values.push("?"); 90 | } 91 | columns = columns.join(','); 92 | values = values.join(','); 93 | return "INSERT INTO `" + table + "` (" + columns + ") VALUES (" + values + ")"; 94 | }; 95 | 96 | MySQLAdapter.prototype.updateSql = function(id, table, data, primaryIndex) { 97 | var c, columns, tuples, val; 98 | columns = []; 99 | for (c in data) { 100 | if (!__hasProp.call(data, c)) continue; 101 | val = data[c]; 102 | columns.push("`" + c + "`=?"); 103 | } 104 | tuples = columns.join(','); 105 | return "UPDATE `" + table + "` SET " + tuples + " WHERE `" + primaryIndex + "` = ?"; 106 | }; 107 | 108 | MySQLAdapter.prototype.getOptions = function(opts) { 109 | var key, options, val, _ref; 110 | options = {}; 111 | _ref = this.defaultOptions; 112 | for (key in _ref) { 113 | if (!__hasProp.call(_ref, key)) continue; 114 | val = _ref[key]; 115 | if (opts[key] != null) { 116 | options[key] = opts[key]; 117 | } else { 118 | options[key] = val; 119 | } 120 | } 121 | return options; 122 | }; 123 | 124 | return MySQLAdapter; 125 | 126 | })(); 127 | 128 | }).call(this); 129 | -------------------------------------------------------------------------------- /test/model.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | 3 | ActiveRecord = require __dirname + "/../lib" 4 | config = new ActiveRecord.Configuration 5 | sqlite: 6 | database: "#{__dirname}/test.db" 7 | 8 | # Model definition 9 | class User extends ActiveRecord.Model 10 | config: config 11 | fields: ['id', 'username', 'name'] 12 | 13 | # Tests 14 | describe 'User', -> 15 | before (done) -> 16 | sqlite3 = require('sqlite3') 17 | db = new sqlite3.Database config.get('sqlite').database 18 | db.serialize -> 19 | db.run "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(20), name VARCHAR(255))", [], done 20 | 21 | after (done) -> 22 | sqlite3 = require('sqlite3') 23 | db = new sqlite3.Database "#{__dirname}/test.db" 24 | db.serialize -> 25 | db.run "DROP TABLE users", [], -> 26 | fs.unlink "#{__dirname}/test.db", done 27 | 28 | describe "#new()", -> 29 | it "should create a new empty user", -> 30 | user = new User() 31 | 32 | user.should.be.an.instanceof User 33 | user.should.have.property 'id', null 34 | user.should.have.property 'name', null 35 | user.should.have.property 'username', null 36 | 37 | it "should create a user with properties", -> 38 | user = new User username: 'foo', name: 'bar' 39 | 40 | user.should.have.property 'id', null 41 | user.should.have.property 'name', 'bar' 42 | user.should.have.property 'username', 'foo' 43 | 44 | describe "#set()", -> 45 | it "should allow updating properties", -> 46 | user = new User username: 'foo', name: 'bar' 47 | user.username = 'foofoo' 48 | user.name = 'barbar' 49 | 50 | user.should.have.property 'name', 'barbar' 51 | user.should.have.property 'username', 'foofoo' 52 | 53 | it "should allow setting undefined fields", -> 54 | user = new User() 55 | user.foo = "bar" 56 | 57 | user.should.have.property 'foo', 'bar' 58 | 59 | describe "#save()", -> 60 | it "should determine the correct table name", -> 61 | user = new User() 62 | user.tableName().should.equal "users" 63 | 64 | it "should save a user record to disk", (done) -> 65 | user = new User username: 'foo', name: 'bar' 66 | user.save done 67 | 68 | it "should update the primary key id", (done) -> 69 | user = new User username: 'foo', name: 'bar' 70 | user.save (err) -> 71 | throw "did not save" if err 72 | user.id.should.be.ok 73 | user.id.should.be.a 'number' 74 | 75 | done() 76 | 77 | describe "#find()", -> 78 | it "should find a model given the primary ID", (done) -> 79 | User.find 1, (err, user) -> 80 | user.should.be.an.instanceof User 81 | user.id.should.equal 1 82 | done() 83 | 84 | it "should find a model given a SQL query", (done) -> 85 | User.find "SELECT * FROM users WHERE id = ?", 1, (err, user) -> 86 | user.should.be.an.instanceof User 87 | user.id.should.equal 1 88 | done() 89 | 90 | describe "#findAll()", -> 91 | it "should find all models given an array of primary IDs", (done) -> 92 | User.findAll [1, 2], (err, users) -> 93 | users.should.be.an.instanceof Array 94 | users.length.should.equal 2 95 | 96 | users[0].should.be.an.instanceof User 97 | users[0].id.should.equal 1 98 | done() 99 | 100 | it "should find all models given a SQL query", (done) -> 101 | User.findAll "SELECT * FROM users", (err, users) -> 102 | users.should.be.an.instanceof Array 103 | users.length.should.be.above 0 104 | users[0].should.be.an.instanceof User 105 | done() 106 | 107 | describe "#update()", -> 108 | it "should update the existing model", (done) -> 109 | User.find 1, (err, user) -> 110 | user.name = "newname" 111 | user.save (err) -> 112 | throw "did not save" if err 113 | user.id.should.equal 1 114 | user.name.should.equal "newname" 115 | 116 | # Re-fetch user 117 | User.find 1, (err, user) -> 118 | user.id.should.equal 1 119 | user.name.should.equal "newname" 120 | done() 121 | 122 | describe "#delete()", -> 123 | it "should delete the model from the DB", (done) -> 124 | User.find 1, (err, user) -> 125 | user.delete (err) -> 126 | throw "did not delete" if err 127 | 128 | user.should.have.property 'id', null 129 | user.should.have.property 'name', null 130 | user.should.have.property 'username', null 131 | 132 | # Attempt to re-fetch user 133 | User.find 1, (err, user) -> 134 | user.should.be.an.instanceof User 135 | user.should.have.property 'id', null 136 | user.isLoaded().should.be.false 137 | done() -------------------------------------------------------------------------------- /lib/activerecord/adapters/sqlite.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var SQLiteAdapter, sqlite3, 3 | __hasProp = {}.hasOwnProperty, 4 | __slice = [].slice; 5 | 6 | sqlite3 = require('sqlite3'); 7 | 8 | module.exports = SQLiteAdapter = (function() { 9 | 10 | SQLiteAdapter.name = 'SQLiteAdapter'; 11 | 12 | SQLiteAdapter.prototype.MIN_SQL_SIZE = 15; 13 | 14 | SQLiteAdapter.prototype.defaultOptions = { 15 | primaryIndex: 'id' 16 | }; 17 | 18 | function SQLiteAdapter(config) { 19 | this.config = config; 20 | this.db = new sqlite3.Database(this.config.database); 21 | } 22 | 23 | SQLiteAdapter.prototype.read = function(finder, table, params, opts, cb) { 24 | var options, sqlClause, 25 | _this = this; 26 | options = this.getOptions(opts); 27 | if (typeof finder === "string" && finder.length >= this.MIN_SQL_SIZE) { 28 | sqlClause = finder; 29 | } else if (Array.isArray(finder)) { 30 | sqlClause = "SELECT * FROM `" + table + "` WHERE `" + options.primaryIndex + "` IN (" + (finder.join(',')) + ")"; 31 | } else { 32 | sqlClause = "SELECT * FROM `" + table + "` WHERE `" + options.primaryIndex + "` = ? LIMIT 1"; 33 | params = [finder]; 34 | } 35 | return this.db.serialize(function() { 36 | return _this.db.all(sqlClause, params, function(err, rows) { 37 | if (err) { 38 | return cb(err, []); 39 | } else { 40 | return cb(null, rows); 41 | } 42 | }); 43 | }); 44 | }; 45 | 46 | SQLiteAdapter.prototype.write = function(id, table, data, newRecord, opts, cb) { 47 | var key, options, params, sqlClause, val, 48 | _this = this; 49 | options = this.getOptions(opts); 50 | params = []; 51 | for (key in data) { 52 | if (!__hasProp.call(data, key)) continue; 53 | val = data[key]; 54 | params.push(val); 55 | } 56 | if (newRecord === true) { 57 | sqlClause = this.insertSql(table, data); 58 | params.unshift(null); 59 | } else { 60 | sqlClause = this.updateSql(id, table, data, options.primaryIndex); 61 | params.push(id); 62 | } 63 | return this.db.serialize(function() { 64 | return _this.db.run(sqlClause, params, function() { 65 | var err, info; 66 | err = arguments[0], info = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 67 | if (err) { 68 | return cb(err); 69 | } else { 70 | return cb(null, this); 71 | } 72 | }); 73 | }); 74 | }; 75 | 76 | SQLiteAdapter.prototype["delete"] = function(id, table, opts, cb) { 77 | var options, sqlClause, 78 | _this = this; 79 | options = this.getOptions(opts); 80 | sqlClause = "DELETE FROM `" + table + "` WHERE `" + options.primaryIndex + "` = ?"; 81 | return this.db.serialize(function() { 82 | return _this.db.run(sqlClause, id, function() { 83 | var err, info; 84 | err = arguments[0], info = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 85 | if (err) { 86 | return cb(err); 87 | } else { 88 | return cb(null, this); 89 | } 90 | }); 91 | }); 92 | }; 93 | 94 | SQLiteAdapter.prototype.insertSql = function(table, data) { 95 | var c, columns, i, val, values, _i, _ref; 96 | columns = ['`id`']; 97 | for (c in data) { 98 | val = data[c]; 99 | columns.push("`" + c + "`"); 100 | } 101 | values = []; 102 | for (i = _i = 0, _ref = columns.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 103 | values.push("?"); 104 | } 105 | columns = columns.join(','); 106 | values = values.join(','); 107 | return "INSERT INTO `" + table + "` (" + columns + ") VALUES (" + values + ")"; 108 | }; 109 | 110 | SQLiteAdapter.prototype.updateSql = function(id, table, data, primaryIndex) { 111 | var c, columns, tuples, val; 112 | columns = []; 113 | for (c in data) { 114 | if (!__hasProp.call(data, c)) continue; 115 | val = data[c]; 116 | columns.push("`" + c + "`=?"); 117 | } 118 | tuples = columns.join(','); 119 | return "UPDATE `" + table + "` SET " + tuples + " WHERE `" + primaryIndex + "` = ?"; 120 | }; 121 | 122 | SQLiteAdapter.prototype.getOptions = function(opts) { 123 | var key, options, val, _ref; 124 | options = {}; 125 | _ref = this.defaultOptions; 126 | for (key in _ref) { 127 | if (!__hasProp.call(_ref, key)) continue; 128 | val = _ref[key]; 129 | if (opts[key] != null) { 130 | options[key] = opts[key]; 131 | } else { 132 | options[key] = val; 133 | } 134 | } 135 | return options; 136 | }; 137 | 138 | return SQLiteAdapter; 139 | 140 | })(); 141 | 142 | }).call(this); 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-activerecord 2 | 3 | [![Build Status](https://secure.travis-ci.org/meltingice/node-activerecord.png?branch=master)](http://travis-ci.org/meltingice/node-activerecord) 4 | 5 | An ORM written in Coffeescript that supports multiple database systems (SQL, NoSQL, and even REST), as well as ID generation middleware. It is fully extendable to add new database systems and plugins. 6 | 7 | **Note:** this project is new and is still evolving rapidly. A lot is done, but there is still a lot to do. 8 | 9 | ## Install 10 | 11 | node-activerecord is available in npm: 12 | 13 | ``` 14 | npm install activerecord 15 | ``` 16 | 17 | Installing node-activerecord will not automatically install the required libraries for every adapter since this could easily make the library very bloated and dependent on things you may or may not need. 18 | 19 | ### Adapter Libraries 20 | 21 | You can use npm to install the required libraries for each adapter: 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
AdapterLibraries
sqlitesqlite3
mysqlmysql
redisredis
RESTrestler
45 | 46 | ### ID Middleware Libraries 47 | 48 | You can also use npm to install the required libraries for any ID generation middleware: 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
MiddlewareLibraries
sqlnone
redisredis
64 | 65 | ### Built-In Plugins 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
PluginEnabled by DefaultDescription
jsontrueAllows conversion of a model to a simple JS object and offers field protection
loggerfalseA verbose logger that outputs significant logging data to the console
84 | 85 | ## Examples 86 | 87 | **Configuration** 88 | 89 | By default, ActiveRecord assumes SQL ID middleware. This means it checks for the last generated auto-increment ID on the primary key. 90 | 91 | ``` coffeescript 92 | ActiveRecord = require 'activerecord' 93 | 94 | module.exports = new ActiveRecord.Configuration 95 | sqlite: 96 | database: "#{__dirname}/test.db" 97 | mysql: 98 | host: 'localhost' 99 | database: 'test' 100 | user: 'test' 101 | password: 'password' 102 | middleware: 103 | redis: 104 | host: 'localhost' 105 | ``` 106 | 107 | **Model Definition** 108 | 109 | ``` coffeescript 110 | ActiveRecord = require 'activerecord' 111 | config = require __dirname + "/config" 112 | 113 | # Note: uses sqlite3 by default 114 | class User extends ActiveRecord.Model 115 | config: config 116 | fields: ['id', 'username', 'name'] 117 | ``` 118 | 119 | **Creating a Record** 120 | 121 | ``` coffeescript 122 | user = new User() 123 | user.username = "meltingice" 124 | user.name = "Ryan" 125 | user.save() 126 | ``` 127 | 128 | **Retreiving a Record** 129 | 130 | ``` coffeescript 131 | # Find by primary ID 132 | User.find 1, (err, user) -> console.log user.toJSON() 133 | 134 | # Find multiple by primary ID 135 | User.findAll [1, 2], (err, users) -> 136 | console.log user.toJSON() for user in users 137 | 138 | # Find by custom query 139 | User.find "SELECT * FROM users WHERE id < ?", 5, (err, user) -> 140 | console.log user.toJSON() 141 | ``` 142 | 143 | **Updating a Record** 144 | 145 | ``` coffeescript 146 | User.find 1, (err, user) -> 147 | user.name = "Bob" 148 | user.save (err) -> console.log "updated!" 149 | ``` 150 | 151 | **Deleting a Record** 152 | 153 | ``` coffeescript 154 | User.find 1, (err, user) -> 155 | user.delete (err) -> console.log "deleted!" 156 | ``` 157 | 158 | **Model Relations** 159 | 160 | ``` coffeescript 161 | class User extends ActiveRecord.Model 162 | config: config 163 | fields: ['id', 'username', 'name'] 164 | hasMany: -> [ 165 | Message 166 | ] 167 | 168 | class Message extends ActiveRecord.Model 169 | config: config 170 | fields: ['id', 'user_id', 'text'] 171 | belongsTo: -> [ 172 | User 173 | ] 174 | 175 | Message.find 1, (message) -> 176 | message.user (err, user) -> 177 | console.log user.toJSON() 178 | ``` 179 | 180 | **Non-SQL Middleware** 181 | 182 | ``` coffeescript 183 | class User extends ActiveRecord.Model 184 | config: config 185 | idMiddleware: 'redis' 186 | idMiddlewareOptions: 187 | key: 'users:id' 188 | 189 | fields: ['id', 'username', 'name'] 190 | ``` 191 | 192 | **Plugins** 193 | 194 | ``` coffeescript 195 | class AltLogger extends ActiveRecord.Plugin 196 | messages: [] 197 | 198 | # Callback hooks 199 | afterCreate: -> messages.push "Created model for #{@tableName()}"; true 200 | afterUpdate: -> messages.push "Updated model for #{@tableName()}"; true 201 | 202 | # Extend the model 203 | outputLog: -> console.log msg for msg in messages 204 | 205 | class User extends ActiveRecord.Model 206 | config: config 207 | fields: ['id', 'username', 'name'] 208 | plugins: -> [ 209 | 'json' 210 | AltLogger 211 | ] 212 | 213 | user = new User name: 'foo', username: 'bar' 214 | user.save (err) -> user.outputLog() 215 | ``` -------------------------------------------------------------------------------- /src/activerecord/model.coffee: -------------------------------------------------------------------------------- 1 | exports.Model = class Model 2 | tableName: "" 3 | 4 | primaryIndex: 'id' 5 | idMiddleware: 'sql' # default 6 | idMiddlewareOptions: {} 7 | 8 | fields: [] 9 | adapters: ["sqlite"] 10 | 11 | # Relationship configuration 12 | _associations: {} 13 | hasMany: -> [] 14 | hasOne: -> [] 15 | belongsTo: -> [] 16 | 17 | # Since the JSON plugin is so commonly used, we include it by 18 | # default. 19 | plugins: -> [ 20 | 'json' 21 | ] 22 | 23 | @find: (args...) -> 24 | return if arguments.length < 1 or arguments[0] is null 25 | 26 | # Use findAll's logic 27 | if typeof args[args.length - 1] is "function" 28 | cb = args.pop() 29 | else 30 | cb = -> 31 | 32 | finished = (err, results) => 33 | if results.length is 0 34 | cb(err, new @) 35 | else 36 | cb(err, results[0]) 37 | 38 | args.push finished 39 | 40 | @findAll.apply @, args 41 | 42 | @findAll: (finder, args...) -> 43 | model = new @ 44 | 45 | if typeof args[args.length - 1] is "function" 46 | cb = args.pop() 47 | else 48 | cb = -> 49 | 50 | # Require the master adapter (first in list) 51 | Adapter = require "#{__dirname}/adapters/#{model.adapters[0]}" 52 | adapter = new Adapter(model.config.get(model.adapters[0])) 53 | 54 | # Query the adapter 55 | results = adapter.read finder, 56 | model.tableName(), 57 | args, 58 | {primaryIndex: model.primaryIndex}, 59 | (err, rows) => 60 | return cb(err, rows) if err 61 | 62 | resultSet = [] 63 | for row in rows 64 | model = new @(row, false) 65 | model.notify 'afterFind' 66 | resultSet.push model 67 | 68 | cb null, resultSet 69 | 70 | @toAssociationName: (plural = false) -> 71 | name = @name.toLowerCase() 72 | if plural then name + "s" else name 73 | 74 | constructor: (data = {}, tainted = true) -> 75 | @_data = {} 76 | @_initData = data 77 | @_dirtyData = {} 78 | @_isDirty = false 79 | @_new = true 80 | 81 | # Plugin system 82 | @pluginCache = [] 83 | @extend @plugins() 84 | 85 | @notify 'beforeInit' 86 | 87 | for field in @fields 88 | do (field) => 89 | # Configure the getter/setters for the model fields 90 | Object.defineProperty @, field, 91 | get: -> @_data[field] 92 | set: (val) -> 93 | if @_data[field] isnt val 94 | filterFunc = "filter" + field.charAt(0).toUpperCase() + field.slice(1) 95 | if @[filterFunc]? and typeof @[filterFunc] is "function" 96 | val = @[filterFunc](val) 97 | 98 | @_data[field] = val 99 | @_dirtyData[field] = val 100 | @_isDirty = true 101 | 102 | enumerable: true 103 | configurable: true 104 | 105 | if @_initData[field] 106 | @_data[field] = @_initData[field] 107 | else 108 | @_data[field] = null 109 | 110 | for type in ['hasOne', 'belongsTo', 'hasMany'] 111 | for association in @[type]() 112 | if Array.isArray association 113 | association = association[0] 114 | 115 | do (association, type) => 116 | assocName = association.toAssociationName(type is 'hasMany') 117 | @[assocName] = (cb) -> @getAssociation association, cb 118 | 119 | if tainted 120 | @_dirtyData = @_initData 121 | @_isDirty = true 122 | 123 | @notify 'afterInit' 124 | 125 | save: (cb = ->) -> 126 | return cb(null) unless @_isDirty 127 | 128 | @notify 'beforeSave', (res) => 129 | cb(null) unless res 130 | 131 | if @isNew() 132 | @notify "beforeCreate" 133 | else 134 | @notify "beforeUpdate" 135 | 136 | return cb(true) unless @isValid() 137 | 138 | if @isNew() and @idMiddleware? 139 | middleware = require "#{__dirname}/middleware/#{@idMiddleware}" 140 | mConfig = @config.get('middleware') 141 | if mConfig?[@idMiddleware] 142 | mOpts = mConfig[@idMiddleware] 143 | else 144 | mOpts = {} 145 | 146 | m = new middleware(mOpts) 147 | 148 | preID = (err, id) => 149 | if id isnt null 150 | @_data[@primaryIndex] = id 151 | @_initData[@primaryIndex] = id 152 | 153 | primaryIndex = @_initData[@primaryIndex] 154 | 155 | for adapter in @adapters 156 | Adapter = require "#{__dirname}/adapters/#{adapter}" 157 | adapter = new Adapter(@config.get(adapter)) 158 | adapter.write primaryIndex, 159 | @tableName(), 160 | @_dirtyData, 161 | @isNew(), 162 | {primaryIndex: @primaryIndex}, 163 | (err, results) => 164 | return cb(err) if err 165 | 166 | if @isNew() and @idMiddleware? and middleware.supports.afterWrite 167 | m.afterWrite @idMiddlewareOptions, results, (err, id) => 168 | postID(err, id, results) 169 | else 170 | postID(null, null, results) 171 | 172 | postID = (err, id, results) => 173 | @_data[@primaryIndex] = id if id isnt null 174 | @_initData[@primaryIndex] = @_data[@primaryIndex] 175 | 176 | if @isNew() 177 | @notify "afterCreate" 178 | else 179 | @notify "afterUpdate" 180 | 181 | @_dirtyData = {} 182 | @_isDirty = false 183 | @_saved = true 184 | @_new = false 185 | 186 | @notify "afterSave", => cb(null) 187 | 188 | if @isNew() and @idMiddleware? and middleware.supports.beforeWrite 189 | m.beforeWrite @idMiddlewareOptions, preID 190 | else 191 | preID(null, null) 192 | 193 | delete: (cb) -> 194 | return cb(true) unless @notify 'beforeDelete' 195 | 196 | for adapter in @adapters 197 | Adapter = require "#{__dirname}/adapters/#{adapter}" 198 | adapter = new Adapter(@config.get(adapter)) 199 | adapter.delete @_data[@primaryIndex], 200 | @tableName(), 201 | {primaryIndex: @primaryIndex}, 202 | (err, result) => 203 | return cb(err) if err 204 | 205 | @_data = {} 206 | @_dirtyData = {} 207 | @_isDirty = false 208 | @_data[field] = null for field in @fields 209 | 210 | @notify 'afterDelete' 211 | cb(null, result) 212 | 213 | # 214 | # Relationships 215 | # 216 | hasOneExists: (model) -> @hasAssociation model, 'hasOne' 217 | hasManyExists: (model) -> @hasAssociation model, 'hasMany' 218 | belongsToExists: (model) -> @hasAssociation model, 'belongsTo' 219 | 220 | hasAssociation: (model, types = ['hasOne', 'hasMany', 'belongsTo']) -> 221 | types = [types] unless Array.isArray(types) 222 | 223 | for type in types 224 | for association in @[type]() 225 | if Array.isArray(association) 226 | return type if association[0].name is model.name 227 | else 228 | return type if association.name is model.name 229 | 230 | return false 231 | 232 | getAssociation: (model, cb) -> 233 | type = @hasAssociation model 234 | return cb(null) if type is false 235 | return cb(null, @_associations[model.name]) if @_associations[model.name]? 236 | 237 | config = @associationConfig model 238 | 239 | internalCb = (err, value) => 240 | return cb(err, value) if err 241 | 242 | if type is "hasMany" and not Array.isArray(value) 243 | value = [value] 244 | 245 | @_associations[model.name] = value 246 | cb(null, value) 247 | 248 | if typeof @[config.loader] is "function" 249 | @[config.loader](internalCb) 250 | else if type in ["hasOne", "belongsTo"] and @hasField(config.foreignKey) 251 | model.find @[config.foreignKey], internalCb 252 | else 253 | internalCb(new model()) 254 | 255 | associationConfig: (model) -> 256 | type = @hasAssociation model 257 | 258 | for assoc in @[type] 259 | if Array.isArray(assoc) 260 | config = assoc[1] 261 | else 262 | config = {} 263 | 264 | defaults = {} 265 | 266 | # Convert to model name 267 | assocName = model.toAssociationName(type is 'hasMany') 268 | assocName = assocName.charAt(0).toUpperCase() + assocName.slice(1) 269 | defaults.foreignKey = model.name.toLowerCase() + "_id" 270 | defaults.loader = "load#{assocName}" 271 | defaults.autoFks = true 272 | 273 | defaults[key] = val for own key, val of config 274 | 275 | return defaults 276 | 277 | saveBelongsToAssociations: (cb) -> 278 | cb(true) if @belongsTo().length is 0 279 | 280 | doneCount = 0 281 | done = => 282 | doneCount++ 283 | cb(true) if doneCount is @belongsTo().length 284 | 285 | for belongsTo in @belongsTo() 286 | unless @_associations[belongsTo.name] 287 | done(); continue 288 | 289 | obj = @_associations[belongsTo.name] 290 | obj.save (err) => 291 | config = @associationConfig(belongsTo) 292 | 293 | if @hasField config.foreignKey 294 | @_data[config.foreignKey] = obj[obj.primaryIndex] 295 | 296 | done() 297 | 298 | saveHasSomeAssociations: (cb) -> 299 | cb(true) if @hasOne().length is 0 and @hasMany().length is 0 300 | 301 | finishCount = @hasOne().length 302 | for hasMany in @hasMany() 303 | if @_associations[hasMany.name] 304 | finishCount += @_associations[hasMany.name].length 305 | else 306 | finishCount++ 307 | 308 | doneCount = 0 309 | done = => 310 | doneCount++ 311 | cb(true) if doneCount is finishCount 312 | 313 | for hasOne in @hasOne() 314 | unless @_associations[hasOne.name] 315 | done(); continue 316 | 317 | obj = @_associations[hasOne.name] 318 | obj.save (err) => 319 | config = @associationConfig(hasOne) 320 | 321 | if @hasField config.foreignKey 322 | @_data[config.foreignKey] = obj[obj.primaryIndex] 323 | 324 | done() 325 | 326 | for hasMany in @hasMany() 327 | unless @_associations[hasMany.name] 328 | done(); continue 329 | 330 | for obj in @_associations[hasMany.name] 331 | obj.save (err) => 332 | config = @associationConfig hasMany 333 | 334 | if @hasField config.foreignKey 335 | obj[config.foreignKey] = @[@primaryIndex] 336 | 337 | done() 338 | 339 | isNew: -> @_new 340 | isLoaded: -> not @isNew() 341 | isDirty: -> @_isDirty 342 | 343 | hasField: (name) -> name in @fields 344 | 345 | tableName: -> 346 | return @table if @table 347 | return @__proto__.constructor.name.toLowerCase() + "s" 348 | 349 | # Extends this model with new functionality. The base of the plugin system. 350 | # In the ES.next future, we can drop directly extending the Model object and 351 | # instead use proxies to send requests to fully separate Plugin objects. 352 | # 353 | # Note that this does not allow you to override any of the existing functions 354 | # in the Model object. 355 | extend: (src) -> 356 | src = [src] unless Array.isArray(src) 357 | 358 | for copy in src 359 | # If it's a string, it's a built-in plugin 360 | if typeof copy is "string" 361 | copy = require __dirname + "/plugins/#{copy}" 362 | 363 | for own prop of copy:: 364 | continue if prop is "constructor" 365 | @[prop] = copy::[prop] unless @[prop] 366 | 367 | @pluginCache.push new copy(@) 368 | 369 | # In the future, this will be used to support notifying plugins as well 370 | notify: (event, cb = null) -> 371 | if cb 372 | # async 373 | @[event] (result1) => 374 | @["_#{event}"] (result2) => 375 | result = result1 and result2 376 | for plugin in @pluginCache 377 | result = result and plugin[event]() 378 | 379 | cb(result) 380 | else 381 | # sync 382 | result = @[event]() and @["_#{event}"]() 383 | for plugin in @pluginCache 384 | result = result and plugin[event]() 385 | 386 | # Internal callbacks. Don't override these. 387 | _isValid: -> true 388 | _beforeInit: -> true 389 | _afterInit: -> true 390 | _afterFind: -> @_new = false; true 391 | _beforeSave: (c) -> @saveBelongsToAssociations(c) 392 | _beforeCreate: -> true 393 | _beforeUpdate: -> true 394 | _afterCreate: -> true 395 | _afterUpdate: -> true 396 | _afterSave: (c) -> @saveHasSomeAssociations(c) 397 | _beforeDelete: -> true 398 | _afterDelete: -> true 399 | 400 | # Callbacks. Override these. 401 | isValid: -> true 402 | beforeInit: -> true 403 | afterInit: -> true 404 | afterFind: -> true 405 | beforeSave: (c) -> c(true) 406 | beforeCreate: -> true 407 | beforeUpdate: -> true 408 | afterCreate: -> true 409 | afterUpdate: -> true 410 | afterSave: (c) -> c(true) 411 | beforeDelete: -> true 412 | afterDelete: -> true -------------------------------------------------------------------------------- /lib/activerecord/model.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Model, 3 | __slice = [].slice, 4 | __hasProp = {}.hasOwnProperty, 5 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 6 | 7 | exports.Model = Model = (function() { 8 | 9 | Model.name = 'Model'; 10 | 11 | Model.prototype.tableName = ""; 12 | 13 | Model.prototype.primaryIndex = 'id'; 14 | 15 | Model.prototype.idMiddleware = 'sql'; 16 | 17 | Model.prototype.idMiddlewareOptions = {}; 18 | 19 | Model.prototype.fields = []; 20 | 21 | Model.prototype.adapters = ["sqlite"]; 22 | 23 | Model.prototype._associations = {}; 24 | 25 | Model.prototype.hasMany = function() { 26 | return []; 27 | }; 28 | 29 | Model.prototype.hasOne = function() { 30 | return []; 31 | }; 32 | 33 | Model.prototype.belongsTo = function() { 34 | return []; 35 | }; 36 | 37 | Model.prototype.plugins = function() { 38 | return ['json']; 39 | }; 40 | 41 | Model.find = function() { 42 | var args, cb, finished, 43 | _this = this; 44 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 45 | if (arguments.length < 1 || arguments[0] === null) { 46 | return; 47 | } 48 | if (typeof args[args.length - 1] === "function") { 49 | cb = args.pop(); 50 | } else { 51 | cb = function() {}; 52 | } 53 | finished = function(err, results) { 54 | if (results.length === 0) { 55 | return cb(err, new _this); 56 | } else { 57 | return cb(err, results[0]); 58 | } 59 | }; 60 | args.push(finished); 61 | return this.findAll.apply(this, args); 62 | }; 63 | 64 | Model.findAll = function() { 65 | var Adapter, adapter, args, cb, finder, model, results, 66 | _this = this; 67 | finder = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 68 | model = new this; 69 | if (typeof args[args.length - 1] === "function") { 70 | cb = args.pop(); 71 | } else { 72 | cb = function() {}; 73 | } 74 | Adapter = require("" + __dirname + "/adapters/" + model.adapters[0]); 75 | adapter = new Adapter(model.config.get(model.adapters[0])); 76 | return results = adapter.read(finder, model.tableName(), args, { 77 | primaryIndex: model.primaryIndex 78 | }, function(err, rows) { 79 | var resultSet, row, _i, _len; 80 | if (err) { 81 | return cb(err, rows); 82 | } 83 | resultSet = []; 84 | for (_i = 0, _len = rows.length; _i < _len; _i++) { 85 | row = rows[_i]; 86 | model = new _this(row, false); 87 | model.notify('afterFind'); 88 | resultSet.push(model); 89 | } 90 | return cb(null, resultSet); 91 | }); 92 | }; 93 | 94 | Model.toAssociationName = function(plural) { 95 | var name; 96 | if (plural == null) { 97 | plural = false; 98 | } 99 | name = this.name.toLowerCase(); 100 | if (plural) { 101 | return name + "s"; 102 | } else { 103 | return name; 104 | } 105 | }; 106 | 107 | function Model(data, tainted) { 108 | var association, field, type, _fn, _fn1, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, 109 | _this = this; 110 | if (data == null) { 111 | data = {}; 112 | } 113 | if (tainted == null) { 114 | tainted = true; 115 | } 116 | this._data = {}; 117 | this._initData = data; 118 | this._dirtyData = {}; 119 | this._isDirty = false; 120 | this._new = true; 121 | this.pluginCache = []; 122 | this.extend(this.plugins()); 123 | this.notify('beforeInit'); 124 | _ref = this.fields; 125 | _fn = function(field) { 126 | return Object.defineProperty(_this, field, { 127 | get: function() { 128 | return this._data[field]; 129 | }, 130 | set: function(val) { 131 | var filterFunc; 132 | if (this._data[field] !== val) { 133 | filterFunc = "filter" + field.charAt(0).toUpperCase() + field.slice(1); 134 | if ((this[filterFunc] != null) && typeof this[filterFunc] === "function") { 135 | val = this[filterFunc](val); 136 | } 137 | this._data[field] = val; 138 | this._dirtyData[field] = val; 139 | return this._isDirty = true; 140 | } 141 | }, 142 | enumerable: true, 143 | configurable: true 144 | }); 145 | }; 146 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 147 | field = _ref[_i]; 148 | _fn(field); 149 | if (this._initData[field]) { 150 | this._data[field] = this._initData[field]; 151 | } else { 152 | this._data[field] = null; 153 | } 154 | } 155 | _ref1 = ['hasOne', 'belongsTo', 'hasMany']; 156 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 157 | type = _ref1[_j]; 158 | _ref2 = this[type](); 159 | _fn1 = function(association, type) { 160 | var assocName; 161 | assocName = association.toAssociationName(type === 'hasMany'); 162 | return _this[assocName] = function(cb) { 163 | return this.getAssociation(association, cb); 164 | }; 165 | }; 166 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 167 | association = _ref2[_k]; 168 | if (Array.isArray(association)) { 169 | association = association[0]; 170 | } 171 | _fn1(association, type); 172 | } 173 | } 174 | if (tainted) { 175 | this._dirtyData = this._initData; 176 | this._isDirty = true; 177 | } 178 | this.notify('afterInit'); 179 | } 180 | 181 | Model.prototype.save = function(cb) { 182 | var _this = this; 183 | if (cb == null) { 184 | cb = function() {}; 185 | } 186 | if (!this._isDirty) { 187 | return cb(null); 188 | } 189 | return this.notify('beforeSave', function(res) { 190 | var m, mConfig, mOpts, middleware, postID, preID; 191 | if (!res) { 192 | cb(null); 193 | } 194 | if (_this.isNew()) { 195 | _this.notify("beforeCreate"); 196 | } else { 197 | _this.notify("beforeUpdate"); 198 | } 199 | if (!_this.isValid()) { 200 | return cb(true); 201 | } 202 | if (_this.isNew() && (_this.idMiddleware != null)) { 203 | middleware = require("" + __dirname + "/middleware/" + _this.idMiddleware); 204 | mConfig = _this.config.get('middleware'); 205 | if (mConfig != null ? mConfig[_this.idMiddleware] : void 0) { 206 | mOpts = mConfig[_this.idMiddleware]; 207 | } else { 208 | mOpts = {}; 209 | } 210 | m = new middleware(mOpts); 211 | } 212 | preID = function(err, id) { 213 | var Adapter, adapter, primaryIndex, _i, _len, _ref, _results; 214 | if (id !== null) { 215 | _this._data[_this.primaryIndex] = id; 216 | _this._initData[_this.primaryIndex] = id; 217 | } 218 | primaryIndex = _this._initData[_this.primaryIndex]; 219 | _ref = _this.adapters; 220 | _results = []; 221 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 222 | adapter = _ref[_i]; 223 | Adapter = require("" + __dirname + "/adapters/" + adapter); 224 | adapter = new Adapter(_this.config.get(adapter)); 225 | _results.push(adapter.write(primaryIndex, _this.tableName(), _this._dirtyData, _this.isNew(), { 226 | primaryIndex: _this.primaryIndex 227 | }, function(err, results) { 228 | if (err) { 229 | return cb(err); 230 | } 231 | if (_this.isNew() && (_this.idMiddleware != null) && middleware.supports.afterWrite) { 232 | return m.afterWrite(_this.idMiddlewareOptions, results, function(err, id) { 233 | return postID(err, id, results); 234 | }); 235 | } else { 236 | return postID(null, null, results); 237 | } 238 | })); 239 | } 240 | return _results; 241 | }; 242 | postID = function(err, id, results) { 243 | if (id !== null) { 244 | _this._data[_this.primaryIndex] = id; 245 | } 246 | _this._initData[_this.primaryIndex] = _this._data[_this.primaryIndex]; 247 | if (_this.isNew()) { 248 | _this.notify("afterCreate"); 249 | } else { 250 | _this.notify("afterUpdate"); 251 | } 252 | _this._dirtyData = {}; 253 | _this._isDirty = false; 254 | _this._saved = true; 255 | _this._new = false; 256 | return _this.notify("afterSave", function() { 257 | return cb(null); 258 | }); 259 | }; 260 | if (_this.isNew() && (_this.idMiddleware != null) && middleware.supports.beforeWrite) { 261 | return m.beforeWrite(_this.idMiddlewareOptions, preID); 262 | } else { 263 | return preID(null, null); 264 | } 265 | }); 266 | }; 267 | 268 | Model.prototype["delete"] = function(cb) { 269 | var Adapter, adapter, _i, _len, _ref, _results, 270 | _this = this; 271 | if (!this.notify('beforeDelete')) { 272 | return cb(true); 273 | } 274 | _ref = this.adapters; 275 | _results = []; 276 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 277 | adapter = _ref[_i]; 278 | Adapter = require("" + __dirname + "/adapters/" + adapter); 279 | adapter = new Adapter(this.config.get(adapter)); 280 | _results.push(adapter["delete"](this._data[this.primaryIndex], this.tableName(), { 281 | primaryIndex: this.primaryIndex 282 | }, function(err, result) { 283 | var field, _j, _len1, _ref1; 284 | if (err) { 285 | return cb(err); 286 | } 287 | _this._data = {}; 288 | _this._dirtyData = {}; 289 | _this._isDirty = false; 290 | _ref1 = _this.fields; 291 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 292 | field = _ref1[_j]; 293 | _this._data[field] = null; 294 | } 295 | _this.notify('afterDelete'); 296 | return cb(null, result); 297 | })); 298 | } 299 | return _results; 300 | }; 301 | 302 | Model.prototype.hasOneExists = function(model) { 303 | return this.hasAssociation(model, 'hasOne'); 304 | }; 305 | 306 | Model.prototype.hasManyExists = function(model) { 307 | return this.hasAssociation(model, 'hasMany'); 308 | }; 309 | 310 | Model.prototype.belongsToExists = function(model) { 311 | return this.hasAssociation(model, 'belongsTo'); 312 | }; 313 | 314 | Model.prototype.hasAssociation = function(model, types) { 315 | var association, type, _i, _j, _len, _len1, _ref; 316 | if (types == null) { 317 | types = ['hasOne', 'hasMany', 'belongsTo']; 318 | } 319 | if (!Array.isArray(types)) { 320 | types = [types]; 321 | } 322 | for (_i = 0, _len = types.length; _i < _len; _i++) { 323 | type = types[_i]; 324 | _ref = this[type](); 325 | for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { 326 | association = _ref[_j]; 327 | if (Array.isArray(association)) { 328 | if (association[0].name === model.name) { 329 | return type; 330 | } 331 | } else { 332 | if (association.name === model.name) { 333 | return type; 334 | } 335 | } 336 | } 337 | } 338 | return false; 339 | }; 340 | 341 | Model.prototype.getAssociation = function(model, cb) { 342 | var config, internalCb, type, 343 | _this = this; 344 | type = this.hasAssociation(model); 345 | if (type === false) { 346 | return cb(null); 347 | } 348 | if (this._associations[model.name] != null) { 349 | return cb(null, this._associations[model.name]); 350 | } 351 | config = this.associationConfig(model); 352 | internalCb = function(err, value) { 353 | if (err) { 354 | return cb(err, value); 355 | } 356 | if (type === "hasMany" && !Array.isArray(value)) { 357 | value = [value]; 358 | } 359 | _this._associations[model.name] = value; 360 | return cb(null, value); 361 | }; 362 | if (typeof this[config.loader] === "function") { 363 | return this[config.loader](internalCb); 364 | } else if ((type === "hasOne" || type === "belongsTo") && this.hasField(config.foreignKey)) { 365 | return model.find(this[config.foreignKey], internalCb); 366 | } else { 367 | return internalCb(new model()); 368 | } 369 | }; 370 | 371 | Model.prototype.associationConfig = function(model) { 372 | var assoc, assocName, config, defaults, key, type, val, _i, _len, _ref; 373 | type = this.hasAssociation(model); 374 | _ref = this[type]; 375 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 376 | assoc = _ref[_i]; 377 | if (Array.isArray(assoc)) { 378 | config = assoc[1]; 379 | } else { 380 | config = {}; 381 | } 382 | } 383 | defaults = {}; 384 | assocName = model.toAssociationName(type === 'hasMany'); 385 | assocName = assocName.charAt(0).toUpperCase() + assocName.slice(1); 386 | defaults.foreignKey = model.name.toLowerCase() + "_id"; 387 | defaults.loader = "load" + assocName; 388 | defaults.autoFks = true; 389 | for (key in config) { 390 | if (!__hasProp.call(config, key)) continue; 391 | val = config[key]; 392 | defaults[key] = val; 393 | } 394 | return defaults; 395 | }; 396 | 397 | Model.prototype.saveBelongsToAssociations = function(cb) { 398 | var belongsTo, done, doneCount, obj, _i, _len, _ref, _results, 399 | _this = this; 400 | if (this.belongsTo().length === 0) { 401 | cb(true); 402 | } 403 | doneCount = 0; 404 | done = function() { 405 | doneCount++; 406 | if (doneCount === _this.belongsTo().length) { 407 | return cb(true); 408 | } 409 | }; 410 | _ref = this.belongsTo(); 411 | _results = []; 412 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 413 | belongsTo = _ref[_i]; 414 | if (!this._associations[belongsTo.name]) { 415 | done(); 416 | continue; 417 | } 418 | obj = this._associations[belongsTo.name]; 419 | _results.push(obj.save(function(err) { 420 | var config; 421 | config = _this.associationConfig(belongsTo); 422 | if (_this.hasField(config.foreignKey)) { 423 | _this._data[config.foreignKey] = obj[obj.primaryIndex]; 424 | } 425 | return done(); 426 | })); 427 | } 428 | return _results; 429 | }; 430 | 431 | Model.prototype.saveHasSomeAssociations = function(cb) { 432 | var done, doneCount, finishCount, hasMany, hasOne, obj, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _results, 433 | _this = this; 434 | if (this.hasOne().length === 0 && this.hasMany().length === 0) { 435 | cb(true); 436 | } 437 | finishCount = this.hasOne().length; 438 | _ref = this.hasMany(); 439 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 440 | hasMany = _ref[_i]; 441 | if (this._associations[hasMany.name]) { 442 | finishCount += this._associations[hasMany.name].length; 443 | } else { 444 | finishCount++; 445 | } 446 | } 447 | doneCount = 0; 448 | done = function() { 449 | doneCount++; 450 | if (doneCount === finishCount) { 451 | return cb(true); 452 | } 453 | }; 454 | _ref1 = this.hasOne(); 455 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 456 | hasOne = _ref1[_j]; 457 | if (!this._associations[hasOne.name]) { 458 | done(); 459 | continue; 460 | } 461 | obj = this._associations[hasOne.name]; 462 | obj.save(function(err) { 463 | var config; 464 | config = _this.associationConfig(hasOne); 465 | if (_this.hasField(config.foreignKey)) { 466 | _this._data[config.foreignKey] = obj[obj.primaryIndex]; 467 | } 468 | return done(); 469 | }); 470 | } 471 | _ref2 = this.hasMany(); 472 | _results = []; 473 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 474 | hasMany = _ref2[_k]; 475 | if (!this._associations[hasMany.name]) { 476 | done(); 477 | continue; 478 | } 479 | _results.push((function() { 480 | var _l, _len3, _ref3, _results1, 481 | _this = this; 482 | _ref3 = this._associations[hasMany.name]; 483 | _results1 = []; 484 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 485 | obj = _ref3[_l]; 486 | _results1.push(obj.save(function(err) { 487 | var config; 488 | config = _this.associationConfig(hasMany); 489 | if (_this.hasField(config.foreignKey)) { 490 | obj[config.foreignKey] = _this[_this.primaryIndex]; 491 | } 492 | return done(); 493 | })); 494 | } 495 | return _results1; 496 | }).call(this)); 497 | } 498 | return _results; 499 | }; 500 | 501 | Model.prototype.isNew = function() { 502 | return this._new; 503 | }; 504 | 505 | Model.prototype.isLoaded = function() { 506 | return !this.isNew(); 507 | }; 508 | 509 | Model.prototype.isDirty = function() { 510 | return this._isDirty; 511 | }; 512 | 513 | Model.prototype.hasField = function(name) { 514 | return __indexOf.call(this.fields, name) >= 0; 515 | }; 516 | 517 | Model.prototype.tableName = function() { 518 | if (this.table) { 519 | return this.table; 520 | } 521 | return this.__proto__.constructor.name.toLowerCase() + "s"; 522 | }; 523 | 524 | Model.prototype.extend = function(src) { 525 | var copy, prop, _i, _len, _ref, _results; 526 | if (!Array.isArray(src)) { 527 | src = [src]; 528 | } 529 | _results = []; 530 | for (_i = 0, _len = src.length; _i < _len; _i++) { 531 | copy = src[_i]; 532 | if (typeof copy === "string") { 533 | copy = require(__dirname + ("/plugins/" + copy)); 534 | } 535 | _ref = copy.prototype; 536 | for (prop in _ref) { 537 | if (!__hasProp.call(_ref, prop)) continue; 538 | if (prop === "constructor") { 539 | continue; 540 | } 541 | if (!this[prop]) { 542 | this[prop] = copy.prototype[prop]; 543 | } 544 | } 545 | _results.push(this.pluginCache.push(new copy(this))); 546 | } 547 | return _results; 548 | }; 549 | 550 | Model.prototype.notify = function(event, cb) { 551 | var plugin, result, _i, _len, _ref, _results, 552 | _this = this; 553 | if (cb == null) { 554 | cb = null; 555 | } 556 | if (cb) { 557 | return this[event](function(result1) { 558 | return _this["_" + event](function(result2) { 559 | var plugin, result, _i, _len, _ref; 560 | result = result1 && result2; 561 | _ref = _this.pluginCache; 562 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 563 | plugin = _ref[_i]; 564 | result = result && plugin[event](); 565 | } 566 | return cb(result); 567 | }); 568 | }); 569 | } else { 570 | result = this[event]() && this["_" + event](); 571 | _ref = this.pluginCache; 572 | _results = []; 573 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 574 | plugin = _ref[_i]; 575 | _results.push(result = result && plugin[event]()); 576 | } 577 | return _results; 578 | } 579 | }; 580 | 581 | Model.prototype._isValid = function() { 582 | return true; 583 | }; 584 | 585 | Model.prototype._beforeInit = function() { 586 | return true; 587 | }; 588 | 589 | Model.prototype._afterInit = function() { 590 | return true; 591 | }; 592 | 593 | Model.prototype._afterFind = function() { 594 | this._new = false; 595 | return true; 596 | }; 597 | 598 | Model.prototype._beforeSave = function(c) { 599 | return this.saveBelongsToAssociations(c); 600 | }; 601 | 602 | Model.prototype._beforeCreate = function() { 603 | return true; 604 | }; 605 | 606 | Model.prototype._beforeUpdate = function() { 607 | return true; 608 | }; 609 | 610 | Model.prototype._afterCreate = function() { 611 | return true; 612 | }; 613 | 614 | Model.prototype._afterUpdate = function() { 615 | return true; 616 | }; 617 | 618 | Model.prototype._afterSave = function(c) { 619 | return this.saveHasSomeAssociations(c); 620 | }; 621 | 622 | Model.prototype._beforeDelete = function() { 623 | return true; 624 | }; 625 | 626 | Model.prototype._afterDelete = function() { 627 | return true; 628 | }; 629 | 630 | Model.prototype.isValid = function() { 631 | return true; 632 | }; 633 | 634 | Model.prototype.beforeInit = function() { 635 | return true; 636 | }; 637 | 638 | Model.prototype.afterInit = function() { 639 | return true; 640 | }; 641 | 642 | Model.prototype.afterFind = function() { 643 | return true; 644 | }; 645 | 646 | Model.prototype.beforeSave = function(c) { 647 | return c(true); 648 | }; 649 | 650 | Model.prototype.beforeCreate = function() { 651 | return true; 652 | }; 653 | 654 | Model.prototype.beforeUpdate = function() { 655 | return true; 656 | }; 657 | 658 | Model.prototype.afterCreate = function() { 659 | return true; 660 | }; 661 | 662 | Model.prototype.afterUpdate = function() { 663 | return true; 664 | }; 665 | 666 | Model.prototype.afterSave = function(c) { 667 | return c(true); 668 | }; 669 | 670 | Model.prototype.beforeDelete = function() { 671 | return true; 672 | }; 673 | 674 | Model.prototype.afterDelete = function() { 675 | return true; 676 | }; 677 | 678 | return Model; 679 | 680 | })(); 681 | 682 | }).call(this); 683 | --------------------------------------------------------------------------------