├── .gitignore ├── LICENSE ├── README.md ├── examples ├── custom-type.js ├── example.js ├── example2.js ├── hooks.js ├── mongodb.js ├── postgres.js ├── predefined-validation.js └── validation.js ├── lib ├── associations │ ├── many.js │ └── one.js ├── database-alias.js ├── databases │ ├── example.js │ ├── helpers.js │ ├── mongodb.js │ ├── mysql.js │ └── postgresql.js ├── events.js ├── hash.js ├── orm.js ├── plugins │ └── debug.js └── validators.js ├── package.json └── test ├── database-helpers.js └── validators.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.sublime-* 4 | Icon.png 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2012 by Diogo Resende 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This project has moved to https://github.com/dresende/node-orm2 2 | -------------------------------------------------------------------------------- /examples/custom-type.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | function MyHashType(value) { 4 | try { 5 | this.hash = JSON.parse(value); 6 | } catch (e) { 7 | this.hash = {}; 8 | } 9 | } 10 | MyHashType.prototype.toString = function () { 11 | return JSON.stringify(this.hash); 12 | } 13 | MyHashType.prototype.newRandomProp = function () { 14 | this.hash.prop = Math.random(); 15 | console.log("meta.prop changed to ", this.hash.prop); 16 | } 17 | 18 | orm.connect("mysql://orm:orm@localhost/orm", function (success, db) { 19 | // define a Person 20 | var Person = db.define("person", { 21 | "created" : Date, 22 | "name" : String, 23 | "surname" : String, 24 | "age" : Number, 25 | "male" : Boolean, 26 | "meta" : MyHashType // this is saved as binary 27 | }, { 28 | "methods" : { 29 | "fullName" :function () { 30 | return this.name + " " + this.surname; 31 | } 32 | } 33 | }); 34 | 35 | Person.sync(); 36 | 37 | createJohn(function (John) { 38 | console.log(John); 39 | console.log("instanceof John.meta == MyHashType ?", John.meta instanceof MyHashType); 40 | 41 | console.log("John.meta.newRandomProp()"); 42 | John.meta.newRandomProp(); 43 | 44 | John.save(function () { 45 | console.log("John saved. Run again to see if meta has changed"); 46 | process.exit(0); 47 | }); 48 | }); 49 | 50 | function createJohn(callback) { 51 | Person.find({ "name": "John" }, function (people) { 52 | if (people === null) { 53 | var John = new Person({ 54 | "name" : "John", 55 | "surname": "Doe", 56 | "created": new Date(), 57 | "age" : 25, 58 | "meta" : new MyHashType("{prop:2}") 59 | }); 60 | John.save(function (err, person) { 61 | callback(person); 62 | }); 63 | } else { 64 | callback(people[0]); 65 | } 66 | }); 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("mysql://orm:orm@localhost/orm", function (success, db) { 4 | if (!success) { 5 | return console.log("Could not connect to database"); 6 | } 7 | 8 | // define a Person 9 | var Person = db.define("person", { 10 | "name" : String, 11 | "surname" : String, 12 | "age" : Number, 13 | "male" : Boolean, 14 | "meta" : Object 15 | }, { 16 | "methods" : { 17 | // this is a method that can be called in any 18 | // instance 19 | "fullName" :function () { 20 | return this.name + " " + this.surname; 21 | } 22 | } 23 | }); 24 | // define a Pet 25 | var Pet = db.define("pet", { 26 | "name" : { "type": "string" }, 27 | "type" : { "type": "enum", "values": [ "dog", "cat", "fish" ] } 28 | }); 29 | 30 | // a Person has many "pets" (from model "Pet") where each one is called a "pet" 31 | Person.hasMany("pets", Pet, "pet", { 32 | link: "Owners", 33 | properties: { 34 | since: Date, 35 | nickname: String 36 | } 37 | }); 38 | // another example: a Group has many "people" (from model "Person") where each one is called a "member" 39 | 40 | // sync to database 41 | Person.sync(); 42 | Pet.sync(); 43 | 44 | Person.plugin("debug"); 45 | 46 | // create the Person John (if it does not exist) 47 | createJohn(function (John) { 48 | // create the Pet Deco (if it does not exist) 49 | createDeco(function (Deco) { 50 | // create the Pet Hugo (if it does not exist) 51 | createHugo(function (Hugo) { 52 | // add Deco and Hugo has John's pets 53 | John.addPets(Deco, Hugo, { 54 | since: new Date(), 55 | nickname: "doggy" 56 | }, function () { 57 | console.log(Deco.name + " and " + Hugo.name + " are now " + John.fullName() + "'s pets"); 58 | 59 | John.getPets(function (pets) { 60 | console.log(pets); 61 | }); 62 | }); 63 | }); 64 | }); 65 | }); 66 | 67 | function createJohn(callback) { 68 | Person.find({ "name": "John" }, function (people) { 69 | if (people === null) { 70 | var John = new Person({ "name": "John", "surname": "Doe", "created": new Date(), "age": 25 }); 71 | John.save(function (err, person) { 72 | callback(person); 73 | }); 74 | } else { 75 | callback(people[0]); 76 | } 77 | }); 78 | } 79 | 80 | function createDeco(callback) { 81 | Pet.find({ "name": "Deco" }, function (pets) { 82 | if (pets === null) { 83 | var Deco = new Pet({ "name": "Deco", "type": "dog" }); 84 | Deco.save(function (err, dog) { 85 | callback(dog); 86 | }); 87 | } else { 88 | callback(pets[0]); 89 | } 90 | }); 91 | } 92 | 93 | function createHugo(callback) { 94 | Pet.find({ "name": "Hugo" }, function (pets) { 95 | if (pets === null) { 96 | var Hugo = new Pet({ "name": "Hugo", "type": "dog" }); 97 | Hugo.save(function (err, dog) { 98 | console.log("Hugo", err, dog); 99 | callback(dog); 100 | }); 101 | } else { 102 | callback(pets[0]); 103 | } 104 | }); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /examples/example2.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("mysql://orm:orm@localhost/orm", function (success, db) { 4 | // define a Pet 5 | var Pet = db.define("pet", { 6 | "name" : { "type": "string" }, 7 | "type" : { "type": "enum", "values": [ "dog", "cat", "fish" ] } 8 | }); 9 | // define a Person 10 | var Person = db.define("person", { 11 | "created" : Date, 12 | "name" : String, 13 | "surname" : String, 14 | "age" : Number, 15 | "male" : Boolean, 16 | "pet" : [ Pet ] // same as: Person.hasMany("pets", Pet, "pet"); 17 | }, { 18 | "methods" : { 19 | "fullName" :function () { 20 | return this.name + " " + this.surname; 21 | } 22 | } 23 | }); 24 | 25 | Person.find(function (people) { 26 | for (var i = 0; i < people.length; i++) { 27 | console.log(people[i].fullName()); 28 | people[i].getPets((function (person) { 29 | return function (pets) { 30 | for (var i = 0; i < pets.length; i++) { 31 | console.log("%s has %s %s", person.fullName(), pets[i].type, pets[i].name) 32 | } 33 | } 34 | })(people[i])); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/hooks.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("mysql://orm:orm@localhost/orm", function (success, db) { 4 | // define a Person 5 | var Person = db.define("person", { 6 | "created" : { "type": "date" }, 7 | "updated_at": { "type": "date" }, 8 | "name" : { "type": "string" }, 9 | "surname" : { "type": "string", "def": "" }, 10 | "age" : { "type": "int" }, 11 | "male" : { "type": "bool", "def": true }, 12 | "meta" : { "type": "struct" } 13 | }, { 14 | "methods" : { 15 | // this is a method that can be called in any 16 | // instance 17 | fullName: function () { 18 | return this.name + " " + this.surname; 19 | } 20 | }, 21 | "hooks": { 22 | beforeSave: function (person) { 23 | person.updated_at = new Date(); 24 | }, 25 | afterSave: function (success, person) { 26 | if (!success) { 27 | return console.log("%s not saved :(", person.fullName()); 28 | } 29 | console.log("%s saved :)", person.fullName()); 30 | } 31 | } 32 | }); 33 | 34 | Person.sync(); 35 | Person.find({ name: "Jane" }, function (Janes) { 36 | if (Janes === null) { 37 | return console.log("No Jane does not exist!"); 38 | } 39 | 40 | var Jane = Janes[0]; 41 | console.log(Jane); 42 | 43 | Jane.age = 20 + Math.round(Math.random() * 10); 44 | 45 | console.log("saving Jane.."); 46 | Jane.save(function (err, SavedJane) { 47 | console.log("error:", err); 48 | console.log("Jane:", SavedJane); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/mongodb.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("mongodb://localhost/dfs", function (success, db) { 4 | if (!success) { 5 | console.log("Error %d: %s", db.number, db.message); 6 | return; 7 | } 8 | 9 | var Doc = db.define("docs", { 10 | "path" : String, 11 | "key" : String, 12 | "meta" : Object, 13 | "copies" : Number, 14 | "size" : Number, 15 | "hash" : String, 16 | "location" : Object 17 | }); 18 | 19 | Doc.find({ key: "mykey" }, function (doc) { 20 | if (doc === null) { 21 | return console.log("no docs found"); 22 | } 23 | console.log("doc with key 'mykey' has id '%s'", doc[0].id); 24 | console.log(doc[0]); 25 | 26 | doc[0].location = [ "nowhere", "somewhere" ]; 27 | doc[0].save(function (err, saved_doc) { 28 | console.log("err?", err); 29 | console.log(saved_doc); 30 | }); 31 | }); 32 | 33 | setTimeout(function () { 34 | console.log("Doc.clear()"); 35 | Doc.clear(); 36 | }, 1e3); 37 | 38 | var mydoc = new Doc({ 39 | "path": "/my/path", 40 | "key" : "mykey", 41 | "meta": { "my": "doc" }, 42 | "copies": 1, 43 | "size": 12345, 44 | "hash": "myhash", 45 | "location": "nowhere" 46 | }); 47 | mydoc.save(function (err, doc) { 48 | console.log(err); 49 | console.log(doc); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/postgres.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("pg://orm:orm@localhost/orm", function (success, db) { 4 | if (!success) { 5 | console.log("Error %d: %s", db.number, db.message); 6 | return; 7 | } 8 | var Person = db.define("person", { 9 | name: String, 10 | created: Date 11 | }); 12 | Person.sync(); 13 | 14 | Person.find({ name: [ "John Doe" ] }, function (people) { 15 | if (people !== null) { 16 | return console.log(people); 17 | } 18 | var John = new Person({ "name": "John Doe", "created": new Date() }); 19 | John.save(function (err, person) { 20 | console.log(person); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/predefined-validation.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("mysql://orm:orm@localhost/orm", function (success, db) { 4 | // define a Person 5 | var Person = db.define("person", { 6 | "created" : { "type": "date" }, 7 | "name" : { "type": "string", "validations": [ orm.validators.unique() ] }, 8 | "surname" : { "type": "string", "def": "" }, 9 | "age" : { "type": "int", "validations": orm.validators.rangeNumber(18) }, 10 | "male" : { "type": "bool", "def": true }, 11 | "meta" : { "type": "struct" } 12 | }, { 13 | "methods" : { 14 | // this is a method that can be called in any 15 | // instance 16 | "fullName" :function () { 17 | return this.name + " " + this.surname; 18 | } 19 | } 20 | }); 21 | Person.find({ name: "Jane" }, function (Janes) { 22 | if (Janes === null) { 23 | return console.log("No Jane does not exist!"); 24 | } 25 | 26 | var Jane = Janes[0]; 27 | 28 | Jane.age = -15; 29 | console.log("saving Jane as -15 year old.."); 30 | Jane.save({ "validateAll": true }, function (err, SavedJane) { 31 | // you should have 2 errors here 32 | console.log("error:", err); 33 | 34 | Jane.age = 19; 35 | console.log("saving Jane as 19 year old.."); 36 | Jane.save(function (err, SavedJane) { 37 | console.log("error:", err); 38 | console.log("Jane:", SavedJane); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/validation.js: -------------------------------------------------------------------------------- 1 | var orm = require(__dirname + "/../lib/orm"); 2 | 3 | orm.connect("mysql://orm:orm@localhost/orm", function (success, db) { 4 | // define a Person 5 | var Person = db.define("person", { 6 | "created" : { "type": "date" }, 7 | "name" : { "type": "string" }, 8 | "surname" : { "type": "string", "def": "" }, 9 | "age" : { 10 | "type": "int", 11 | "validations": [ ageValidation ] 12 | }, 13 | "male" : { "type": "bool", "def": true }, 14 | "meta" : { "type": "struct" } 15 | }, { 16 | "methods" : { 17 | // this is a method that can be called in any 18 | // instance 19 | "fullName" :function () { 20 | return this.name + " " + this.surname; 21 | } 22 | }, 23 | "validations": { 24 | "age": ageValidation 25 | } 26 | }); 27 | 28 | function ageValidation(age, next) { 29 | // we could just make it sync but 30 | // here I'm just simulating an async call 31 | // that takes 1 second 32 | setTimeout(function () { 33 | if (age < 18) return next('underage'); 34 | 35 | return next(); 36 | }, 1e3); 37 | } 38 | 39 | Person.find({ name: "Jane" }, function (Janes) { 40 | if (Janes === null) { 41 | return console.log("No Jane does not exist!"); 42 | } 43 | 44 | var Jane = Janes[0]; 45 | 46 | Jane.age = 15; 47 | console.log("saving Jane as 15 year old.."); 48 | Jane.save(function (err, SavedJane) { 49 | console.log("error:", err); 50 | 51 | Jane.age = 19; 52 | console.log("saving Jane as 19 year old.."); 53 | Jane.save(function (err, SavedJane) { 54 | console.log("error:", err); 55 | console.log("Jane:", SavedJane); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /lib/associations/many.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | define: function (orm, Model, model, fields, colParams, plugins) { 3 | var idProperty = colParams && colParams.idProperty ? colParams.idProperty : "id"; 4 | 5 | return { 6 | extend: function (associations) { 7 | var helper = this; 8 | 9 | Model.hasMany = function () { 10 | var model = this; 11 | var association = null; 12 | var field = ""; 13 | var opts = {}; 14 | 15 | for (var i = 0; i < arguments.length; i++) { 16 | switch (typeof arguments[i]) { 17 | case "string": 18 | if (association === null) { 19 | association = arguments[i]; 20 | } 21 | field = arguments[i]; 22 | break; 23 | case "function": 24 | model = arguments[i]; 25 | break; 26 | case "object": 27 | opts = arguments[i]; 28 | break; 29 | } 30 | } 31 | 32 | associations.push({ 33 | "field" : association, 34 | "name" : field, 35 | "type" : "many", 36 | "model" : model, // this = circular reference 37 | "opts" : opts 38 | }); 39 | 40 | helper.create(this, association, field, model, opts); 41 | }; 42 | }, 43 | create: function (instance, association, field, associationModel, associationOpts) { 44 | var camelCaseAssociation = association.substr(0, 1).toUpperCase() + association.substr(1); 45 | var collection = associationOpts.collection ? associationOpts.collection : model + "_" + association; 46 | 47 | instance.prototype["add" + camelCaseAssociation] = function () { 48 | var instances = [], cb = null; 49 | var data = {}, extra = {}; 50 | data[model + "_id"] = this[idProperty]; 51 | 52 | for (var i = 0; i < arguments.length; i++) { 53 | switch (typeof arguments[i]) { 54 | case "function": 55 | cb = arguments[i]; 56 | break; 57 | case "object": 58 | if (arguments[i].hasOwnProperty("_dataPending")) { 59 | instances.push(arguments[i]); 60 | } else { 61 | extra = arguments[i]; 62 | } 63 | break; 64 | default: 65 | } 66 | } 67 | 68 | for (var k in extra) { 69 | data[k] = extra[k]; 70 | } 71 | 72 | if (instances.length === 0) { 73 | throw { "message": "No " + camelCaseAssociation + " were given" }; 74 | } 75 | 76 | var missingInstances = instances.length; 77 | 78 | instances.forEach(function (instance) { 79 | if (!instance.saved()) { 80 | instance.save(function (err, savedInstance) { 81 | if (err) return cb(err); 82 | 83 | data[field + "_id"] = savedInstance[idProperty]; 84 | 85 | orm._db.saveRecord(null, collection, data, function (err) { 86 | if (plugins.hasOwnProperty("afterSave")) { 87 | for (var k in plugins["afterSave"]) { 88 | plugins["afterSave"][k](data, Model); 89 | } 90 | } 91 | if (--missingInstances === 0) cb(null); 92 | }); 93 | }); 94 | return; 95 | } 96 | 97 | data[field + "_id"] = instance[idProperty]; 98 | orm._db.saveRecord(null, collection, data, function (err) { 99 | if (plugins.hasOwnProperty("afterSave")) { 100 | for (var k in plugins["afterSave"]) { 101 | plugins["afterSave"][k](data, Model); 102 | } 103 | } 104 | if (--missingInstances === 0) cb(null); 105 | }); 106 | }); 107 | }; 108 | 109 | instance.prototype["remove" + camelCaseAssociation] = function () { 110 | var instances = []; 111 | var cb = null; 112 | var data = {}; 113 | 114 | data[model + "_id"] = this[idProperty]; 115 | 116 | for (var i = 0; i < arguments.length; i++) { 117 | if (typeof arguments[i] == "function") { 118 | cb = arguments[i]; 119 | } else { 120 | instances.push(arguments[i]); 121 | } 122 | } 123 | 124 | if (instances.length === 0) { 125 | orm._db.clearRecords(collection, { 126 | "conditions" : data, 127 | "callback" : function () { 128 | cb(null); 129 | } 130 | }); 131 | return; 132 | } 133 | 134 | var missingInstances = instances.length; 135 | 136 | instances.forEach(function (instance) { 137 | if (typeof instance[idProperty] == "undefined" || instance[idProperty] === 0) { 138 | if (--missingInstances === 0) cb(null); 139 | return; 140 | } 141 | 142 | data[field + "_id"] = instance[idProperty]; 143 | 144 | orm._db.clearRecords(collection, { 145 | "conditions" : data, 146 | "callback" : function () { 147 | if (--missingInstances === 0) cb(null); 148 | } 149 | }); 150 | }); 151 | }; 152 | 153 | instance.prototype["set" + camelCaseAssociation] = function () { 154 | var instances = []; 155 | var cb = null; 156 | var data = {}; 157 | 158 | data[model + "_id"] = this[idProperty]; 159 | 160 | for (var i = 0; i < arguments.length; i++) { 161 | if (typeof arguments[i] == "function") { 162 | cb = arguments[i]; 163 | } else { 164 | instances.push(arguments[i]); 165 | } 166 | } 167 | 168 | orm._db.clearRecords(collection, { 169 | "conditions" : data, 170 | "callback" : function () { 171 | if (instances.length === 0) return cb(null); 172 | 173 | var missingInstances = instances.length; 174 | 175 | instances.forEach(function (instance) { 176 | if (!instance.saved()) { 177 | instance.save(function (err, savedInstance) { 178 | if (err) return cb(err); 179 | 180 | data[field + "_id"] = savedInstance[idProperty]; 181 | 182 | orm._db.saveRecord(null, collection, data, function (err) { 183 | if (--missingInstances === 0) cb(null); 184 | }); 185 | }); 186 | return; 187 | } 188 | 189 | data[field + "_id"] = instance[idProperty]; 190 | orm._db.saveRecord(null, collection, data, function (err) { 191 | if (--missingInstances === 0) cb(null); 192 | }); 193 | }); 194 | } 195 | }); 196 | }; 197 | 198 | instance.prototype["get" + camelCaseAssociation] = function () { 199 | var cb = null; 200 | var opts = {}; 201 | 202 | for (var i = 0; i < arguments.length; i++) { 203 | switch (typeof arguments[i]) { 204 | case "object": 205 | opts = arguments[i]; 206 | break; 207 | case "function": 208 | cb = arguments[i]; 209 | break; 210 | } 211 | } 212 | 213 | orm._db.selectRecords(associationModel._ORM.collection, { 214 | "rel": [{ 215 | collection: collection, 216 | rel: [ "id", field + "_id", model + "_id" ], 217 | value: this[idProperty] 218 | }], 219 | "conditions": {}, 220 | "callback" : function (err, data) { 221 | if (err) return cb(null); 222 | 223 | var pending = data.length; 224 | var checkDone = function () { 225 | if (pending-- == 1) { 226 | cb(data); 227 | } 228 | }; 229 | 230 | for (var i = 0; i < data.length; i++) { 231 | data[i] = new associationModel(data[i], opts); 232 | data[i].ready(checkDone); 233 | } 234 | } 235 | }); 236 | 237 | return this; 238 | }; 239 | 240 | if (associationOpts.link && associationOpts.link.length) { 241 | var associationLink = associationOpts.link; 242 | 243 | associationModel.prototype["get" + associationLink] = function () { 244 | var cb = null; 245 | var conditions = {}; 246 | 247 | for (var i = 0; i < arguments.length; i++) { 248 | switch (typeof arguments[i]) { 249 | case "object": 250 | conditions = arguments[i]; 251 | break; 252 | case "function": 253 | cb = arguments[i]; 254 | break; 255 | } 256 | } 257 | 258 | orm._db.selectRecords(Model._ORM.collection, { 259 | "rel": [{ 260 | collection: collection, 261 | rel: [ "id", model + "_id", field + "_id" ], 262 | value: this[idProperty] 263 | }], 264 | "conditions": conditions, 265 | "callback" : function (err, data) { 266 | if (err) return cb(null); 267 | 268 | var pending = data.length; 269 | var checkDone = function () { 270 | if (pending-- == 1) { 271 | cb(data); 272 | } 273 | }; 274 | 275 | for (var i = 0; i < data.length; i++) { 276 | data[i] = new Model(data[i]); 277 | data[i].ready(checkDone); 278 | } 279 | } 280 | }); 281 | }; 282 | } 283 | }, 284 | fetch: function (instance, assoc, opts) { 285 | if (!(assoc.opts && 286 | assoc.opts.hasOwnProperty("autoFetch") && 287 | assoc.opts.autoFetch === true && 288 | (!opts.hasOwnProperty("fetchDepth") || opts.fetchDepth > 0))) { 289 | return; 290 | } 291 | var camelCaseAssociation = assoc.field.substr(0, 1).toUpperCase() + assoc.field.substr(1); 292 | var assocOpts = {}; 293 | 294 | for (var k in opts) { 295 | if (k == "fetchDepth") { 296 | assocOpts.fetchDepth = opts.fetchDepth - 1; 297 | } else { 298 | assocOpts[k] = opts[k]; 299 | } 300 | } 301 | 302 | instance._dataPending += 1; 303 | instance[assoc.field] = []; 304 | instance["get" + camelCaseAssociation](assocOpts, function (result) { 305 | instance[assoc.field] = result; 306 | 307 | if (instance._dataPending-- == 1) { 308 | instance.emit("ready", instance); 309 | } 310 | }); 311 | } 312 | }; 313 | } 314 | }; 315 | -------------------------------------------------------------------------------- /lib/associations/one.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | define: function (orm, Model, model, fields, colParams, plugins) { 3 | var idProperty = colParams && colParams.idProperty ? colParams.idProperty : "id"; 4 | 5 | return { 6 | extend: function (associations) { 7 | var helper = this; 8 | 9 | Model.hasOne = function () { 10 | var model = this; 11 | var association = "association"; 12 | var opts = {}; 13 | 14 | for (var i = 0; i < arguments.length; i++) { 15 | switch (typeof arguments[i]) { 16 | case "string": 17 | association = arguments[i]; 18 | break; 19 | case "function": 20 | model = arguments[i]; 21 | break; 22 | case "object": 23 | opts = arguments[i]; 24 | break; 25 | } 26 | } 27 | 28 | associations.push({ 29 | "field" : association, 30 | "type" : "one", 31 | "model" : model, // this = circular reference 32 | "opts" : opts 33 | }); 34 | 35 | helper.create(this, association, model); 36 | }; 37 | }, 38 | create: function (instance, association, associationModel) { 39 | var camelCaseAssociation = association.substr(0, 1).toUpperCase() + association.substr(1); 40 | 41 | instance.prototype["get" + camelCaseAssociation] = function () { 42 | var cb = null, opts = {}; 43 | 44 | for (var i = 0; i < arguments.length; i++) { 45 | switch (typeof arguments[i]) { 46 | case "object": 47 | opts = arguments[i]; 48 | break; 49 | case "function": 50 | cb = arguments[i]; 51 | break; 52 | } 53 | } 54 | 55 | if (this[association + "_id"] > 0) { 56 | if (this[association]) return cb(this[association]); 57 | 58 | var data = {}, conditions = {}; 59 | data[model + "_id"] = this[idProperty]; 60 | 61 | conditions[idProperty] = this[association + "_id"]; 62 | 63 | orm._db.selectRecords(associationModel._ORM.collection, { 64 | "conditions": conditions, 65 | "callback" : function (err, data) { 66 | if (err || !data || data.length === 0) return cb(null); 67 | 68 | (new associationModel(data[0], opts)).ready(cb); 69 | } 70 | }); 71 | return; 72 | } 73 | cb(null); 74 | }; 75 | instance.prototype["unset" + camelCaseAssociation] = function (cb) { 76 | this["set" + camelCaseAssociation](null, cb); 77 | }; 78 | instance.prototype["set" + camelCaseAssociation] = function (instance, cb) { 79 | var self = this; 80 | 81 | if (instance === null) { 82 | self[association + "_id"] = 0; 83 | delete self[association]; 84 | 85 | return cb(); 86 | } 87 | 88 | if (!instance.saved()) { 89 | instance.save(function (err, savedInstance) { 90 | if (err) return cb(err); 91 | 92 | self[association + "_id"] = savedInstance[idProperty]; 93 | self[association] = savedInstance; 94 | cb(); 95 | }); 96 | return; 97 | } 98 | self[association + "_id"] = instance[idProperty]; 99 | self[association] = instance; 100 | cb(); 101 | }; 102 | }, 103 | fetch: function (instance, assoc, opts) { 104 | if (!(instance[assoc.field + "_id"] > 0 && 105 | assoc.opts && assoc.opts.hasOwnProperty("autoFetch") && 106 | assoc.opts.autoFetch === true && 107 | (!opts.hasOwnProperty("fetchDepth") || opts.fetchDepth > 0))) { 108 | return; 109 | } 110 | var camelCaseassoc = assoc.field.substr(0, 1).toUpperCase() + assoc.field.substr(1); 111 | var assocOpts = {}; 112 | 113 | for (var k in opts) { 114 | if (k == "fetchDepth") { 115 | assocOpts.fetchDepth = opts.fetchDepth - 1; 116 | } else { 117 | assocOpts[k] = opts[k]; 118 | } 119 | } 120 | 121 | instance._dataPending += 1; 122 | instance[assoc.field] = null; 123 | instance["get" + camelCaseassoc](assocOpts, function (result) { 124 | instance[assoc.field] = result; 125 | 126 | if (instance._dataPending-- == 1) { 127 | instance.emit("ready", instance); 128 | } 129 | }); 130 | } 131 | }; 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /lib/database-alias.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pg": "postgresql", 3 | "postgres": "postgresql" 4 | }; 5 | -------------------------------------------------------------------------------- /lib/databases/example.js: -------------------------------------------------------------------------------- 1 | var DBClient = function () { 2 | /** 3 | * Object constructor. This can have all the parameters you want, 4 | * it will only be used by the module. 5 | **/ 6 | }; 7 | DBClient.prototype.createCollection = function (collection, fields, assocs) { 8 | /** 9 | * Create collection with fields and assoc(iation)s 10 | **/ 11 | }; 12 | DBClient.prototype.selectRecords = function (collection, config) { 13 | /** 14 | * Get records from a collection. config is a hash that can have: 15 | * - conditions: a hash with key=value AND conditions 16 | * - order: order string like "field (asc|desc)[, ..]*" 17 | * - limit: a limit amount of records to fetch 18 | * - skip: an amount of records to skip from start 19 | **/ 20 | }; 21 | DBClient.prototype.clearRecords = function (collection, config, callback) { 22 | /** 23 | * Remove records from a collection. config is a hash that can have 24 | * the same keys as .selectRecords (check comment above). 25 | **/ 26 | }; 27 | DBClient.prototype.saveRecord = function (idProp, collection, data, callback) { 28 | /** 29 | * Save a record. data is a hash with record data. if data.id exists, 30 | * the record should be updated, otherwise created. callback is to 31 | * be called after .saveRecord finishes. 32 | * idProp is the name of the primary key field 33 | **/ 34 | }; 35 | DBClient.prototype.end = function () { 36 | /** 37 | * Close the connection to the database. Do not reuse the db object. 38 | **/ 39 | }; 40 | 41 | exports.connect = function (options, callback) { 42 | /** 43 | * Initiate communication with the database. options is a hash with: 44 | * - host: hostname to connect 45 | * - port: port to use 46 | * - auth: username:password to use 47 | * - pathname: /database to use 48 | * 49 | * This options are parsed from a url string like: proto://auth@hostname:/pathname. 50 | * Some parameters might not be present so you should ensure you have defaults. 51 | * 52 | * callback should be called with 2 parameters, where the first one is a boolean 53 | * variable saying if connection was success or not. If not success, the second 54 | * parameter should be an object with .number and .message keys saying what happened. 55 | * If success, the second parameter should be a reference to the DBClient above. 56 | **/ 57 | }; 58 | -------------------------------------------------------------------------------- /lib/databases/helpers.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | var events = require("events"); 3 | var moment = require("moment"); 4 | 5 | var re_order_mode = /^(a|de)sc$/i; 6 | 7 | function DBQuery(query) { 8 | events.EventEmitter.call(this); 9 | 10 | query.on("row", (function (o) { 11 | return function (row) { 12 | o.emit("record", row); 13 | }; 14 | })(this)); 15 | query.on("result", (function (o) { 16 | return function (row) { 17 | o.emit("record", row); 18 | }; 19 | })(this)); 20 | query.on("end", (function (o) { 21 | return function (res) { 22 | o.emit("end", res); 23 | }; 24 | })(this)); 25 | query.on("error", (function (o) { 26 | return function (err) { 27 | o.emit("error", err); 28 | }; 29 | })(this)); 30 | } 31 | util.inherits(DBQuery, events.EventEmitter); 32 | 33 | module.exports = { 34 | DBQuery: DBQuery, 35 | 36 | // order should be in the format "][ [ ] ..]" 37 | // - must be "asc" or "desc" 38 | buildSqlOrder: function (order, escapeCb) { 39 | var orders = [], i = 0; 40 | 41 | order = order.replace(/\s+$/, '').replace(/^\s+/, '').split(/\s+/); 42 | 43 | for (; i < order.length; i++) { 44 | if (i < order.length - 1 && re_order_mode.test(order[i + 1])) { 45 | orders.push((escapeCb ? escapeCb(order[i]) : order[i]) + " " + order[i + 1].toUpperCase()); 46 | i += 1; 47 | } else { 48 | orders.push((escapeCb ? escapeCb(order[i]) : order[i]) + " ASC"); 49 | } 50 | } 51 | 52 | return " ORDER BY " + orders.join(", "); 53 | }, 54 | // both limit and skip should be numbers, skip is optional 55 | buildSqlLimit: function (limit, skip) { 56 | if (skip) { 57 | return " LIMIT " + skip + ", " + limit; 58 | } 59 | return " LIMIT " + limit; 60 | }, 61 | buildSqlWhere: function (conditions, escapeCb, opts) { 62 | var _conditions = [], _values = [], prop, op, i; 63 | var token_n = 1; 64 | 65 | if (!opts) opts = {}; 66 | 67 | for (var k in conditions) { 68 | if (!conditions.hasOwnProperty(k)) continue; 69 | 70 | if (k.indexOf(" ") > 0) { 71 | op = k.substr(k.indexOf(" ") + 1, k.length).replace(/^\s+/, ""); 72 | prop = k.substr(0, k.indexOf(" ")); 73 | 74 | if (Array.isArray(opts.additional_operators) && opts.additional_operators.indexOf(op.toUpperCase()) >= 0) { 75 | op = " " + op.toUpperCase() + " "; 76 | } else if ([ "=", "!", "!=", ">", "<", ">=", "<=" ].indexOf(op) == -1) { 77 | op = "="; 78 | } else if (op == "!") { 79 | op = "!="; 80 | } 81 | } else { 82 | prop = k; 83 | op = "="; 84 | } 85 | 86 | if (escapeCb) prop = escapeCb(prop); 87 | 88 | if (typeof conditions[k] == "boolean") { 89 | _conditions.push(prop + op + (opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 90 | if (typeof opts.boolean_convert == "function") { 91 | _values.push(opts.boolean_convert(conditions[k])); 92 | } else { 93 | _values.push(conditions[k] ? 1 : 0); 94 | } 95 | continue; 96 | } 97 | if (Array.isArray(conditions[k])) { 98 | if (conditions[k].length > 0) { 99 | var tmp = []; 100 | for (i = 0; i < conditions[k].length; i++) { 101 | tmp.push((opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 102 | } 103 | _conditions.push(prop + " " + (op == "!=" ? "NOT " : "") + "IN (" + tmp.join(",") + ")"); 104 | _values = _values.concat(conditions[k]); 105 | } else { 106 | // ? 107 | _conditions.push(prop + " " + (op == "!=" ? "NOT " : "") + "IN (NULL)"); 108 | } 109 | continue; 110 | } 111 | if (typeof conditions[k] == "object" && conditions[k].hasOwnProperty("__ormFunction")) { 112 | _conditions.push(conditions[k].__ormFunction.replace(/\#\#/g, escapeCb(prop)) + op + (opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 113 | _values.push(conditions[k].v); 114 | continue; 115 | } 116 | _conditions.push(prop + op + (opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 117 | _values.push(conditions[k]); 118 | } 119 | 120 | return [ " WHERE " + _conditions.join(" AND "), _values ]; 121 | }, 122 | 123 | createSqlUpdate: function (opts) { 124 | var query = "UPDATE " + opts.escape(opts.table) + 125 | " SET %values WHERE " + opts.escape(opts.key) + " = " + opts.id; 126 | 127 | query = query.replace("%values", opts.info.query); 128 | 129 | // console.log(query, opts.info.values); 130 | return opts.db.query(query, opts.info.values, this.handleSqlUpdateCall(opts.callback)); 131 | }, 132 | 133 | createSqlInsert: function (opts) { 134 | var query = "INSERT INTO " + opts.escape(opts.table) + " (%fields) " + 135 | "VALUES (%values)"; 136 | 137 | query = query.replace("%fields", Object.keys(opts.data).map(opts.escape).join(", ")); 138 | query = query.replace("%values", opts.info.escapes.join(", ")); 139 | 140 | // console.log(query, opts.info.values); 141 | return opts.db.query(query, opts.info.values, this.handleSqlInsertCall(opts.callback)); 142 | }, 143 | 144 | handleSqlUpdateCall: function (cb) { 145 | return function (err, info) { 146 | if (err) { 147 | return cb(err); 148 | } 149 | return cb(null); 150 | }; 151 | }, 152 | 153 | handleSqlInsertCall: function (cb, orm) { 154 | return function (err, info) { 155 | if (err) { 156 | return cb(err); 157 | } 158 | return cb(null, info.insertId); 159 | }; 160 | }, 161 | 162 | escapeUpdateFields: function (data, escapeCb, opts) { 163 | var val = [], id = [], token_n = 1; 164 | 165 | for (var k in data) { 166 | if (!data.hasOwnProperty(k)) continue; 167 | 168 | if (typeof data[k] == "boolean") { 169 | id.push(escapeCb(k) + " = " + (opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 170 | val.push(opts.boolean_convert(data[k])); 171 | continue; 172 | } 173 | if (data[k] === null) { 174 | id.push(escapeCb(k) + " = " + (opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 175 | val.push(null); 176 | continue; 177 | } 178 | if (util.isDate(data[k])) { 179 | id.push(escapeCb(k) + " = " + opts.date_convert_fmt); 180 | val.push(moment.utc(data[k]).unix()); 181 | continue; 182 | } 183 | id.push(escapeCb(k) + " = " + (opts.tokenCb ? opts.tokenCb(token_n++) : "?")); 184 | val.push(data[k]); 185 | } 186 | 187 | return { query: id.join(", "), values: val }; 188 | }, 189 | escapeInsertFields: function (data, opts) { 190 | var val = [], escapes = [], token_n = 1; 191 | 192 | for (var k in data) { 193 | if (!data.hasOwnProperty(k)) continue; 194 | 195 | if (typeof data[k] == "boolean") { 196 | escapes.push(opts.tokenCb ? opts.tokenCb(token_n++) : "?"); 197 | val.push(opts.boolean_convert(data[k])); 198 | continue; 199 | } 200 | if (data[k] === null) { 201 | escapes.push(opts.tokenCb ? opts.tokenCb(token_n++) : "?"); 202 | val.push(null); 203 | continue; 204 | } 205 | if (util.isDate(data[k])) { 206 | escapes.push(opts.date_convert_fmt); 207 | val.push(moment.utc(data[k]).unix()); 208 | continue; 209 | } 210 | escapes.push(opts.tokenCb ? opts.tokenCb(token_n++) : "?"); 211 | val.push(data[k]); 212 | } 213 | 214 | return { values: val, escapes: escapes }; 215 | } 216 | }; 217 | -------------------------------------------------------------------------------- /lib/databases/mongodb.js: -------------------------------------------------------------------------------- 1 | var DBClient = function (client) { 2 | this.mongodb = require("mongodb"); 3 | this._client = client; 4 | this._collections = {}; 5 | }; 6 | 7 | DBClient.prototype.getClient = function () { 8 | return this._client; 9 | }; 10 | DBClient.prototype.getCollection = function (collection, cb) { 11 | collection = collection.toLowerCase(collection); 12 | 13 | if (this._collections.hasOwnProperty(collection)) { 14 | return cb(this._collections[collection]); 15 | } 16 | 17 | this._client.collection(collection, (function (dbclient) { 18 | return function (err, col) { 19 | if (err) { 20 | throw new Error("Could not get collection '" + collection + "'"); 21 | } 22 | dbclient._collections[collection] = col; 23 | 24 | cb(col); 25 | }; 26 | })(this)); 27 | }; 28 | 29 | DBClient.prototype.createCollection = function (collection, fields, assocs, callback) { 30 | var _collection = collection.toLowerCase(collection); 31 | this._client.createCollection(_collection, function () { 32 | if (callback !== undefined) { 33 | callback(true); 34 | } 35 | }); 36 | 37 | return; 38 | }; 39 | DBClient.prototype.selectRecords = function (collection, config) { 40 | config = config || {}; 41 | 42 | this.getCollection(collection, function (col) { 43 | var k, prop, op; 44 | for (k in config.conditions) { 45 | if (!config.conditions.hasOwnProperty(k)) continue; 46 | if (k.indexOf(" ") <= 0) { 47 | // handle arrays before continue.. 48 | if (Array.isArray(config[k])) { 49 | config[k] = { "$in": config[k] }; 50 | } 51 | continue; 52 | } 53 | 54 | op = k.substr(k.indexOf(" ") + 1, k.length).replace(/^\s+/, ""); 55 | prop = k.substr(0, k.indexOf(" ")); 56 | 57 | if ([ "=", "!", ">", "<", ">=", "<=" ].indexOf(op) == -1) { 58 | op = "="; 59 | } else if (op == "!") { 60 | op = "!="; 61 | } 62 | 63 | switch (op) { 64 | case "=": config[prop] = (Array.isArray(config[k]) ? { "$in": config[k] } : config[k]); break; 65 | case "!=": config[prop] = (Array.isArray(config[k]) ? { "$nin": config[k] } : { "$ne": config[k] }); break; 66 | case ">": config[prop] = { "$gt": config[k] }; break; 67 | case "<": config[prop] = { "$lt": config[k] }; break; 68 | case ">=": config[prop] = { "$gte": config[k] }; break; 69 | case "<=": config[prop] = { "$lte": config[k] }; break; 70 | } 71 | delete config[k]; 72 | } 73 | 74 | var cursor = col.find(config.conditions || {}); 75 | 76 | if (config.order) { 77 | // @TODO: better parse.. 78 | cursor.sort([ config.order.split(" ", 2) ]); 79 | } 80 | if (config.limit) cursor.limit(config.limit); 81 | if (config.skip) cursor.skip(config.skip); 82 | 83 | cursor.toArray(function (err, docs) { 84 | if (err) { 85 | config.callback(err); 86 | return; 87 | } 88 | 89 | for (var i = 0; i < docs.length; i++) { 90 | docs[i].id = docs[i]._id.toHexString(); 91 | delete docs[i]._id; 92 | } 93 | config.callback(null, docs); 94 | }); 95 | }); 96 | }; 97 | DBClient.prototype.clearRecords = function (collection, config, callback) { 98 | if (config.order || config.skip || config.limit) { 99 | throw new Error("MongoDB clearRecords() not fully done yet. Can't use order/skip/limit"); 100 | } 101 | if (config.conditions) { 102 | if (config.conditions.hasOwnProperty("id")) { 103 | config.conditions._id = this._client.bson_serializer.ObjectID.createFromHexString(config.conditions.id); 104 | delete config.conditions.id; 105 | } 106 | } 107 | this.getCollection(collection, function (col) { 108 | col.remove(config.conditions || {}, { "safe": true }, config.callback); 109 | }); 110 | }; 111 | DBClient.prototype.saveRecord = function (idProp, collection, data, callback) { 112 | this.getCollection(collection, (function (dbclient) { 113 | return function (col) { 114 | if (data.hasOwnProperty(idProp)) { 115 | var id = dbclient._client.bson_serializer.ObjectID.createFromHexString(data[idProp]); 116 | delete data[idProp]; 117 | 118 | col.update({ "_id": id }, data, { "safe": true, "upsert": true }, function (err) { 119 | if (err) { 120 | return callback(err); 121 | } 122 | 123 | data[idProp] = id.toHexString(); 124 | 125 | return callback(null, data); 126 | }); 127 | } else { 128 | col.insert(data, { "safe": true }, function (err, objs) { 129 | if (err) { 130 | return callback(err); 131 | } 132 | 133 | objs[0][idProp] = objs[0]._id.toHexString(); 134 | delete objs[0]._id; 135 | 136 | return callback(null, objs[0]); 137 | }); 138 | } 139 | }; 140 | })(this)); 141 | }; 142 | DBClient.prototype.end = function () { 143 | // exists? 144 | //this._client.end(); 145 | }; 146 | 147 | exports.connect = function (options, callback) { 148 | var mongodb = require("mongodb"); 149 | var client; 150 | var opts = { 151 | "host" : "localhost", 152 | "port" : 27017, 153 | "user" : "root", 154 | "password" : "", 155 | "database" : "test" 156 | }; 157 | 158 | if (options.auth) { 159 | var p; 160 | if ((p = options.auth.indexOf(":")) != -1) { 161 | options.user = options.auth.substr(0, p); 162 | options.password = options.auth.substr(p + 1); 163 | } else { 164 | options.user = options.auth; 165 | } 166 | } 167 | if (options.pathname) { 168 | options.database = options.pathname.substr(1); 169 | } 170 | if (options.hostname) { 171 | options.host = options.hostname; 172 | } 173 | 174 | for (k in options) { 175 | if (opts.hasOwnProperty(k)) { 176 | opts[k] = options[k]; 177 | } 178 | } 179 | 180 | opts.port = parseInt(opts.port, 10); 181 | 182 | client = new mongodb.Db(opts.database, new mongodb.Server(opts.host, opts.port, {})); 183 | client.open(function (err, cli) { 184 | if (err) { 185 | return callback(false, err); 186 | } 187 | 188 | return callback(true, new DBClient(cli)); 189 | }); 190 | }; 191 | 192 | exports.use_db = function (rawDb, callback) { 193 | callback(true, new DBClient(rawDb)); 194 | }; 195 | -------------------------------------------------------------------------------- /lib/databases/mysql.js: -------------------------------------------------------------------------------- 1 | var mysql = require("mysql"); 2 | var mysql_v2 = (typeof mysql.createConnection == "function"); 3 | var helpers = require("./helpers"); 4 | 5 | function DBClient(client) { 6 | this._client = client; 7 | } 8 | DBClient.prototype.getClient = function () { 9 | return this._client; 10 | }; 11 | DBClient.prototype.createCollection = function (collection, fields, assocs, opts, callback) { 12 | var _table = collection.toLowerCase(); 13 | var _query = "", _fields = [], _indexes = []; 14 | var k, assoc_fields; 15 | 16 | var field_names = []; 17 | var add_id = (!fields._params || fields._params.indexOf("no-id") == -1); 18 | var unique_record = (fields._params && fields._params.indexOf("unique-record") != -1); 19 | 20 | opts = opts || {}; 21 | if (!opts.hasOwnProperty("engine")) { 22 | opts.engine = "InnoDB"; 23 | } 24 | if (!opts.hasOwnProperty("encoding")) { 25 | opts.encoding = "utf8"; 26 | } 27 | if (!opts.hasOwnProperty("collate")) { 28 | opts.collate = "utf8_general_ci"; 29 | } 30 | _query = "CREATE TABLE IF NOT EXISTS " + this._escapeId("%table") + " (%values) " + 31 | "ENGINE = " + opts.engine + 32 | " CHARACTER SET " + opts.encoding + 33 | " COLLATE " + opts.collate; 34 | 35 | _query = _query.replace("%table", _table); 36 | 37 | if (add_id) { 38 | _fields.push("`id` BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT"); 39 | } 40 | 41 | for (var i = 0; i < assocs.length; i++) { 42 | switch (assocs[i].type) { 43 | case "one": 44 | _fields.push(this._escapeId(assocs[i].field + "_id") + " BIGINT(10) UNSIGNED NOT NULL"); 45 | _indexes.push(assocs[i].field + "_id"); 46 | field_names.push(assocs[i].field + "_id"); 47 | break; 48 | case "many": 49 | assoc_fields = { 50 | "_params": [ "unique-record", "no-id" ] 51 | }; 52 | if (assocs[i].opts && assocs[i].opts.properties) { 53 | for (k in assocs[i].opts.properties) { 54 | if (!assocs[i].opts.properties.hasOwnProperty(k)) continue; 55 | 56 | assoc_fields[k] = assocs[i].opts.properties[k]; 57 | } 58 | } 59 | this.createCollection(_table + "_" + assocs[i].field, assoc_fields, [{ 60 | "field" : _table, 61 | "type" : "one", 62 | "entity": this 63 | }, { 64 | "field" : assocs[i].name || assocs[i].field, 65 | "type" : "one", 66 | "entity": assocs[i].entity 67 | }]); 68 | break; 69 | } 70 | } 71 | 72 | for (k in fields) { 73 | if (k == "_params") { 74 | continue; 75 | } 76 | 77 | var field = this._escapeId(k); 78 | 79 | switch (fields[k].type) { 80 | case "enum": 81 | field += " ENUM ('" + fields[k].values.join("', '") + "')"; 82 | break; 83 | case "struct": 84 | case "object": 85 | case "text": field += " TEXT"; break; 86 | case "num": 87 | case "number": 88 | case "int": 89 | case "integer": field += " INT"; break; 90 | case "float": field += " FLOAT"; break; 91 | case "bool": 92 | case "boolean": field += " TINYINT(1)"; break; 93 | case "date": field += " DATETIME"; 94 | if (!fields[k].hasOwnProperty("allowNull")) { 95 | // if not set, dates are better nulled than 0000-00-00 .. 96 | fields[k].allowNull = true; 97 | } 98 | break; 99 | case "data": field += " BLOB"; break; 100 | default: 101 | field += " VARCHAR(255)"; 102 | } 103 | 104 | if (fields[k].hasOwnProperty("default")) { 105 | field += " DEFAULT '" + fields[k]["default"] + "'"; 106 | } 107 | if (!fields[k].hasOwnProperty("allowNull") || !fields[k].allowNull) { 108 | field += " NOT NULL"; 109 | } 110 | 111 | //field_names.push(k); 112 | _fields.push(field); 113 | } 114 | 115 | if (add_id) { 116 | _fields.push("PRIMARY KEY (" + this._escapeId("id") + ")"); 117 | } 118 | 119 | if (unique_record) { 120 | _fields.push("PRIMARY KEY (" + field_names.map(this._escapeId).join(", ") + ")"); 121 | } else { 122 | for (i = 0; i < _indexes.length; i++) { 123 | _fields.push("INDEX (" + this._escapeId(_indexes[i]) + ")"); 124 | } 125 | } 126 | 127 | _query = _query.replace("%values", _fields.join(", ")); 128 | 129 | this._client.query(_query, function (err, info) { 130 | if (callback !== undefined) { 131 | var success = !err; 132 | callback(success); 133 | } 134 | }); 135 | }; 136 | DBClient.prototype.selectRecords = function (collection, config) { 137 | var query = "SELECT * FROM "; 138 | var query_tables = this._escapeId(this._collectionToTable(collection)) + " t0"; 139 | var values = []; 140 | var tmp; 141 | 142 | config = config || {}; 143 | 144 | if (config.rel && config.rel.length) { 145 | for (var i = 0; i < config.rel.length; i++) { 146 | query_tables = "(" + query_tables + ") JOIN " + 147 | this._escapeId(config.rel[i].collection) + " t" + (i + 1) + 148 | " ON " + this._escapeId("t" + i + "." + config.rel[i].rel[0]) + 149 | " = " + this._escapeId("t" + (i + 1) + "." + config.rel[i].rel[1]); 150 | config.conditions["t" + (i + 1) + "." + config.rel[i].rel[2]] = config.rel[i].value; 151 | } 152 | } 153 | 154 | query += query_tables; 155 | 156 | if (config.conditions) { 157 | tmp = this._addQueryConditions(config.conditions); 158 | query += tmp[0]; 159 | values = values.concat(tmp[1]); 160 | } 161 | if (config.order) { 162 | query += helpers.buildSqlOrder(config.order, this._escapeId); 163 | } 164 | if (config.limit) { 165 | query += helpers.buildSqlLimit(config.limit, config.skip); 166 | } 167 | 168 | //console.log(query, values); 169 | 170 | if (typeof config.callback == "function") { 171 | this._client.query(query, values, function (err, info) { 172 | if (err) { 173 | config.callback(err); 174 | return; 175 | } 176 | 177 | config.callback(null, info); 178 | }); 179 | } else { 180 | return new helpers.DBQuery(this._client.query(query, values)); 181 | } 182 | }; 183 | DBClient.prototype.clearRecords = function (collection, config) { 184 | var query = "DELETE FROM " + this._escapeId(this._collectionToTable(collection)); 185 | var tmp; 186 | var values = []; 187 | 188 | config = config || {}; 189 | 190 | if (config.conditions) { 191 | tmp = this._addQueryConditions(config.conditions); 192 | query += tmp[0]; 193 | values = values.concat(tmp[1]); 194 | } 195 | if (config.order) { 196 | query += helpers.buildSqlOrder(config.order, this._escapeId); 197 | } 198 | if (config.limit) { 199 | query += helpers.buildSqlLimit(config.limit, config.skip); 200 | } 201 | 202 | this._client.query(query, values, function (err, info) { 203 | if (err) { 204 | config.callback(err); 205 | return; 206 | } 207 | 208 | config.callback(null, info); 209 | }); 210 | }; 211 | DBClient.prototype.saveRecord = function (propertyId, collection, data, cb) { 212 | if (propertyId && data[propertyId] && parseInt(data[propertyId], 10) > 0) { 213 | var id = data[propertyId]; 214 | delete data[propertyId]; 215 | 216 | this._updateRecord(collection, propertyId, data, id, cb); 217 | } else { 218 | this._insertRecord(collection, data, cb); 219 | } 220 | }; 221 | DBClient.prototype._insertRecord = function (collection, data, cb) { 222 | return helpers.createSqlInsert({ 223 | table: this._collectionToTable(collection), 224 | escape: this._escapeId, 225 | info: helpers.escapeInsertFields(data, this._helperOpts()), 226 | orm: this._orm, 227 | data: data, 228 | db: this._client, 229 | callback: cb 230 | }); 231 | }; 232 | DBClient.prototype._updateRecord = function (collection, propertyId, data, id, cb) { 233 | return helpers.createSqlUpdate({ 234 | table: this._collectionToTable(collection), 235 | key: propertyId, 236 | id: id, 237 | escape: this._escapeId, 238 | info: helpers.escapeUpdateFields(data, this._escapeId, this._helperOpts()), 239 | db: this._client, 240 | callback: cb 241 | }); 242 | }; 243 | DBClient.prototype._addQueryConditions = function (conditions) { 244 | return helpers.buildSqlWhere(conditions, this._escapeId, this._helperOpts()); 245 | }; 246 | DBClient.prototype._collectionToTable = function (collection) { 247 | return collection.toLowerCase(); 248 | }; 249 | DBClient.prototype._helperOpts = function () { 250 | return { 251 | date_convert_fmt: "FROM_UNIXTIME(?)", 252 | additional_operators: [ "LIKE", "ILIKE" ], 253 | boolean_convert: this._booleanToSqlValue 254 | }; 255 | }; 256 | DBClient.prototype._booleanToSqlValue = function (value) { 257 | return value ? 1 : 0; 258 | }; 259 | DBClient.prototype._escapeId = function (id) { 260 | if (id.indexOf(".") == -1) { 261 | return "`" + id + "`"; 262 | } else { 263 | return "`" + id.substr(0, id.indexOf(".")) + "`.`" + id.substr(id.indexOf(".") + 1) + "`"; 264 | } 265 | }; 266 | DBClient.prototype.end = function () { 267 | this._client.end(); 268 | }; 269 | 270 | exports.connect = function (options, callback) { 271 | var client = null; 272 | var opts = { 273 | "host" : "localhost", 274 | "port" : 3306, 275 | "user" : "root", 276 | "password" : "", 277 | "database" : "test", 278 | "charset" : "UTF8_GENERAL_CI" 279 | }; 280 | 281 | if (options.auth) { 282 | var p; 283 | if ((p = options.auth.indexOf(":")) != -1) { 284 | options.user = options.auth.substr(0, p); 285 | options.password = options.auth.substr(p + 1); 286 | } else { 287 | options.user = options.auth; 288 | } 289 | } 290 | if (options.pathname) { 291 | options.database = options.pathname.substr(1); 292 | } 293 | if (options.hostname) { 294 | options.host = options.hostname; 295 | } 296 | 297 | for (var k in options) { 298 | if (opts.hasOwnProperty(k)) { 299 | opts[k] = options[k]; 300 | } 301 | } 302 | 303 | if (mysql_v2) { 304 | client = mysql.createConnection(opts); 305 | client.connect(function (err) { 306 | if (err) { 307 | return callback(false, err); 308 | } 309 | return callback(true, new DBClient(client)); 310 | }); 311 | } else { 312 | client = mysql.createClient(opts); 313 | 314 | client.query("SHOW STATUS"); 315 | 316 | var testConnection = function (n) { 317 | if (n <= 0) { 318 | return callback(false); 319 | } 320 | if (client.connected) { 321 | return callback(true, new DBClient(client)); 322 | } 323 | 324 | setTimeout(function () { 325 | testConnection(n - 1); 326 | }, 250); 327 | }; 328 | 329 | testConnection(12); // 12 means 12 tries every 250ms = 3 seconds timeout 330 | } 331 | }; 332 | 333 | exports.use_db = function (rawDb, callback) { 334 | callback(true, new DBClient(rawDb)); 335 | }; 336 | -------------------------------------------------------------------------------- /lib/databases/postgresql.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | var events = require("events"); 3 | var helpers = require("./helpers"); 4 | 5 | function DBQuery(query) { 6 | events.EventEmitter.call(this); 7 | 8 | query.on("row", (function (o) { 9 | return function (row) { 10 | o.emit("record", row); 11 | }; 12 | })(this)); 13 | query.on("end", (function (o) { 14 | return function (res) { 15 | o.emit("end", res); 16 | }; 17 | })(this)); 18 | query.on("error", (function (o) { 19 | return function (err) { 20 | o.emit("error", err); 21 | }; 22 | })(this)); 23 | } 24 | util.inherits(DBQuery, events.EventEmitter); 25 | 26 | function DBClient(client) { 27 | this._client = client; 28 | } 29 | 30 | DBClient.prototype.getClient = function () { 31 | return this._client; 32 | }; 33 | 34 | DBClient.prototype.createCollection = function (collection, fields, assocs, opts, callback) { /* opts not used yet */ 35 | var _table = collection.toLowerCase(); 36 | var _query = "CREATE TABLE \"%table\" (%values)", _fields = [], _indexes = []; 37 | _query = _query.replace("%table", _table); 38 | 39 | var field_names = [], self = this; 40 | var add_id = (!fields._params || fields._params.indexOf("no-id") == -1); 41 | var unique_record = (fields._params && fields._params.indexOf("unique-record") != -1); 42 | var text_search_fields = [], emptycb = function () {}; 43 | 44 | if (add_id) { 45 | _fields.push("\"id\" SERIAL"); 46 | } 47 | for (var k in fields) { 48 | if (k == "_params") { 49 | continue; 50 | } 51 | 52 | var field = "\"" + k + "\""; 53 | 54 | if (fields[k].textsearch) { 55 | text_search_fields.push(k); 56 | } 57 | 58 | switch (fields[k].type) { 59 | case "enum": 60 | field += " ENUM ('" + fields[k].values.join("', '") + "')"; 61 | break; 62 | case "struct": 63 | case "object": 64 | case "text": field += " TEXT"; break; 65 | case "num": 66 | case "number": 67 | case "int": 68 | case "integer": field += " INTEGER"; break; 69 | case "float": field += " REAL"; break; 70 | case "bool": 71 | case "boolean": field += " BOOLEAN"; break; 72 | case "date": field += " TIMESTAMP"; break; 73 | case "data": field += " BYTEA"; break; 74 | default: 75 | field += " VARCHAR(255)"; 76 | } 77 | 78 | if (fields[k].hasOwnProperty("default")) { 79 | field += " DEFAULT '" + fields[k]["default"] + "'"; 80 | } 81 | if (!fields[k].hasOwnProperty("allowNull") || !fields[k].allowNull) { 82 | field += " NOT NULL"; 83 | } 84 | field_names.push(k); 85 | _fields.push(field); 86 | } 87 | 88 | for (var i = 0; i < assocs.length; i++) { 89 | switch (assocs[i].type) { 90 | case "one": 91 | _fields.push("\"" + assocs[i].field + "_id\" INTEGER NOT NULL"); 92 | _indexes.push(assocs[i].field + "_id"); 93 | field_names.push(assocs[i].field + "_id"); 94 | break; 95 | case "many": 96 | this.createCollection(_table + "_" + assocs[i].field, { 97 | "_params": [ "unique-record", "no-id" ] 98 | }, [{ 99 | "field" : _table, 100 | "type" : "one", 101 | "entity": this 102 | }, { 103 | "field" : assocs[i].name || assocs[i].field, 104 | "type" : "one", 105 | "entity": assocs[i].entity 106 | }]); 107 | break; 108 | } 109 | } 110 | 111 | if (add_id) { 112 | _fields.push("PRIMARY KEY (\"id\")"); 113 | } 114 | 115 | if (unique_record) { 116 | _fields.push("PRIMARY KEY (\"" + field_names.join("\", \"") + "\")"); 117 | } 118 | 119 | _query = _query.replace("%values", _fields.join(", ")); 120 | //console.log(_query); 121 | 122 | this._client.query(_query, function (err, info) { 123 | if (text_search_fields.length > 0) { 124 | self._client.query("CREATE INDEX \"" + _table + "_idx\" " + 125 | "ON \""+_table+"\" " + 126 | "USING gin(to_tsvector('english', " + text_search_fields.join(" || ' ' || ") + "))", function () {}); 127 | } 128 | if (!unique_record) { 129 | for (i = 0; i < _indexes.length; i++) { 130 | self._client.query("CREATE INDEX \"" + _table + "_" + _indexes[i] + "_idx\" " + "ON \"" + _table + "\"(" + _indexes[i] + ")", emptycb); 131 | } 132 | } 133 | 134 | if (callback !== undefined) { 135 | var success = !err; 136 | callback(success); 137 | } 138 | }); 139 | }; 140 | DBClient.prototype.selectRecords = function (collection, config) { 141 | var query = "SELECT * FROM "; 142 | var query_tables = this._escapeId(this._collectionToTable(collection)) + " t0"; 143 | var values = []; 144 | var tmp; 145 | 146 | config = config || {}; 147 | 148 | if (config.rel && config.rel.length) { 149 | for (var i = 0; i < config.rel.length; i++) { 150 | query_tables = query_tables + " JOIN " + 151 | this._escapeId(config.rel[i].collection) + " t" + (i + 1) + 152 | " ON " + this._escapeId("t" + i + "." + config.rel[i].rel[0]) + 153 | " = " + this._escapeId("t" + (i + 1) + "." + config.rel[i].rel[1]); 154 | config.conditions["t" + (i + 1) + "." + config.rel[i].rel[2]] = config.rel[i].value; 155 | } 156 | } 157 | 158 | query += query_tables; 159 | 160 | if (config.conditions) { 161 | tmp = this._addQueryConditions(config.conditions); 162 | query += tmp[0]; 163 | values = values.concat(tmp[1]); 164 | } 165 | if (config.order) { 166 | query += helpers.buildSqlOrder(config.order, this._escapeId); 167 | } 168 | if (config.limit) { 169 | query += helpers.buildSqlLimit(config.limit, config.skip); 170 | } 171 | 172 | if (typeof config.callback == "function") { 173 | this._client.query(query, values, function (err, info) { 174 | if (err) { 175 | config.callback(err); 176 | return; 177 | } 178 | 179 | config.callback(null, info.rows); 180 | }); 181 | } else { 182 | return new DBQuery(this._client.query(query, values)); 183 | } 184 | }; 185 | // this should not be used for now.. 186 | DBClient.prototype.searchRecords = function (collection, config) { 187 | var _table = collection.toLowerCase(collection); 188 | var _query = "SELECT * FROM \"" + _table + "\" WHERE to_tsvector(body) @@ to_tsquery('%text')"; 189 | 190 | config.text = config.text.replace(/\Wand\W/gi, " & "); 191 | config.text = config.text.replace(/\Wor\W/gi, " | "); 192 | config.text = config.text.replace(/\Wnot\W/gi, " !"); 193 | _query = _query.replace("%text", config.text.replace("'", "''")); 194 | 195 | if (config.limit) { 196 | query += helpers.buildSqlLimit(config.limit, config.skip); 197 | } 198 | 199 | //console.log(_query); 200 | 201 | this._client.query(_query, function (err, info) { 202 | if (err) { 203 | config.callback(err); 204 | return; 205 | } 206 | 207 | config.callback(null, info.rows); 208 | }); 209 | }; 210 | DBClient.prototype.clearRecords = function (collection, config, callback) { 211 | var query = "DELETE FROM " + this._escapeId(this._collectionToTable(collection)); 212 | var tmp; 213 | var values = []; 214 | 215 | config = config || {}; 216 | 217 | if (config.conditions) { 218 | tmp = this._addQueryConditions(config.conditions); 219 | query += tmp[0]; 220 | values = values.concat(tmp[1]); 221 | } 222 | if (config.order) { 223 | query += helpers.buildSqlOrder(config.order, this._escapeId); 224 | } 225 | if (config.limit) { 226 | query += helpers.buildSqlLimit(config.limit, config.skip); 227 | } 228 | 229 | this._client.query(query, values, function (err, info) { 230 | if (err) { 231 | config.callback(err); 232 | return; 233 | } 234 | 235 | config.callback(null, info.rows); 236 | }); 237 | }; 238 | DBClient.prototype.saveRecord = function (propertyId, collection, data, callback) { 239 | if (data[propertyId] && parseInt(data[propertyId], 10) > 0) { 240 | var id = data[propertyId]; 241 | delete data[propertyId]; 242 | 243 | this._updateRecord(collection, propertyId, data, id, callback); 244 | } else { 245 | this._insertRecord(collection, propertyId, data, callback); 246 | } 247 | }; 248 | DBClient.prototype._insertRecord = function (collection, propertyId, data, cb) { 249 | var self = this; 250 | 251 | return helpers.createSqlInsert({ 252 | table: this._collectionToTable(collection), 253 | escape: this._escapeId, 254 | info: helpers.escapeInsertFields(data, this._helperOpts()), 255 | orm: this._orm, 256 | data: data, 257 | db: this._client, 258 | callback: function (err, info) { 259 | if (err) { 260 | return cb(err); 261 | } 262 | 263 | self._client.query("SELECT CURRVAL(pg_get_serial_sequence('" + 264 | self._collectionToTable(collection) + "', '" + 265 | propertyId + "'))", function (err, info) { 266 | if (err) { 267 | return cb(err); 268 | } 269 | 270 | return cb(null, info.rows[0].currval); 271 | }); 272 | } 273 | }); 274 | }; 275 | DBClient.prototype._updateRecord = function (collection, propertyId, data, id, cb) { 276 | return helpers.createSqlUpdate({ 277 | table: this._collectionToTable(collection), 278 | key: propertyId, 279 | id: id, 280 | escape: this._escapeId, 281 | info: helpers.escapeUpdateFields(data, this._escapeId, this._helperOpts()), 282 | db: this._client, 283 | callback: cb 284 | }); 285 | }; 286 | DBClient.prototype._addQueryConditions = function (conditions) { 287 | return helpers.buildSqlWhere(conditions, this._escapeId, this._helperOpts()); 288 | }; 289 | DBClient.prototype._collectionToTable = function (collection) { 290 | return collection.toLowerCase(); 291 | }; 292 | DBClient.prototype._helperOpts = function () { 293 | return { 294 | additional_operators: [ "LIKE", "ILIKE" ], 295 | date_convert_fmt: "TO_TIMESTAMP(?)::timestamp", 296 | boolean_convert: this._booleanToSqlValue, 297 | tokenCb: function (n) { return '$' + n; } 298 | }; 299 | }; 300 | DBClient.prototype._booleanToSqlValue = function (value) { 301 | return value ? 1 : 0; 302 | }; 303 | DBClient.prototype._escapeId = function (id) { 304 | if (id.indexOf(".") == -1) { 305 | return "\"" + id + "\""; 306 | } else { 307 | return "\"" + id.substr(0, id.indexOf(".")) + "\".\"" + id.substr(id.indexOf(".") + 1) + "\""; 308 | } 309 | }; 310 | DBClient.prototype.end = function () { 311 | this._client.end(); 312 | }; 313 | 314 | exports.connect = function (options, callback) { 315 | var pg = require("pg"); 316 | 317 | if (options.auth) { 318 | var p; 319 | if ((p = options.auth.indexOf(":")) != -1) { 320 | options.user = options.auth.substr(0, p); 321 | options.password = options.auth.substr(p + 1); 322 | } else { 323 | options.user = options.auth; 324 | } 325 | } 326 | if (options.pathname) { 327 | options.database = options.pathname.substr(1); 328 | } 329 | if (options.hostname) { 330 | options.host = options.hostname; 331 | } 332 | 333 | var connectionString = "pg://" 334 | + (options.user ? options.user : "") 335 | + (options.password ? ":" + options.password : "") 336 | + (options.host ? "@" + options.host : "") 337 | + (options.port ? ":" + options.port : "") 338 | + "/" + options.database; 339 | // console.log('connection = ' + connectionString); 340 | var client = new pg.Client(connectionString); 341 | var errorListener = function (err) { 342 | callback(false, { "number": parseInt(err.code, 10), "message": err.message }); 343 | }; 344 | client.once("error", errorListener); 345 | client.once("connect", function () { 346 | client.removeListener("error", errorListener); 347 | callback(true, new DBClient(client)); 348 | }); 349 | client.connect(); 350 | }; 351 | 352 | exports.use_db = function (rawDb, callback) { 353 | callback(true, new DBClient(rawDb)); 354 | }; 355 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extend: function (Model) { 3 | Model._eventListeners = {}; 4 | 5 | Model.on = function (ev, cb) { 6 | if (!this._eventListeners.hasOwnProperty(ev)) { 7 | this._eventListeners[ev] = []; 8 | } 9 | this._eventListeners[ev].push(cb); 10 | return this; 11 | }; 12 | Model.emit = function () { 13 | var args = Array.prototype.slice.call(arguments), 14 | ev = args.splice(0, 1); 15 | 16 | if (!this._eventListeners.hasOwnProperty(ev)) return; 17 | 18 | for (var i = 0; i < this._eventListeners[ev].length; i++) { 19 | this._eventListeners[ev][i].call(this, args); 20 | } 21 | //console.log("event '%s'", ev, args); 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /lib/hash.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hash: function (data) { 3 | var h = require("crypto").createHash("md5"); 4 | h.update(JSON.stringify(data)); 5 | 6 | return h.digest("hex"); 7 | } 8 | } -------------------------------------------------------------------------------- /lib/orm.js: -------------------------------------------------------------------------------- 1 | var crypto = require("crypto"); 2 | var moment = require("moment"); 3 | var util = require("util"); 4 | var events = require("events"); 5 | var db_alias = require("./database-alias"); 6 | 7 | function ORM(db) { 8 | this._db = db; 9 | this._models = {}; 10 | } 11 | ORM.prototype.model = function (model) { 12 | return this._models.hasOwnProperty(model) ? this._models[model] : null; 13 | }; 14 | ORM.prototype.end = function () { 15 | this._db.end(); 16 | }; 17 | ORM.prototype.getConnection = function () { 18 | return this._db; 19 | }; 20 | ORM.prototype.getClient = function () { 21 | return this._db.getClient(); 22 | }; 23 | ORM.prototype.define = function (model, fields, colParams) { 24 | var orm = this; 25 | var associations = []; 26 | var associationHelpers = {}; 27 | var idProperty = colParams && colParams.idProperty ? colParams.idProperty : "id"; 28 | var plugins = {}; 29 | 30 | var Model = function (data, opts) { 31 | events.EventEmitter.call(this); 32 | 33 | this._dataPending = 0; 34 | this._dataHash = null; 35 | 36 | data || (data = {}); 37 | opts || (opts = {}); 38 | 39 | for (var k in fields) { 40 | if (!fields.hasOwnProperty(k)) { 41 | continue; 42 | } 43 | if (!data.hasOwnProperty(k) && fields[k].hasOwnProperty("def")) { 44 | this[k] = fields[k].def; 45 | } 46 | } 47 | 48 | for (k in data) { 49 | if (!data.hasOwnProperty(k)) continue; 50 | 51 | if (!fields.hasOwnProperty(k) && k != idProperty) { 52 | // this was wrong, I don't think we should preserve 53 | // undescribed properties 54 | //this[k] = data[k]; 55 | if (k.substr(-3) != "_id") { 56 | if (!this.hasOwnProperty("link")) { 57 | this.link = {}; 58 | } 59 | this.link[k] = data[k]; 60 | } 61 | continue; 62 | } 63 | 64 | if (k == idProperty) { 65 | // fixed property 66 | Object.defineProperty(this, idProperty, { 67 | "value": data[idProperty], 68 | "enumerable": true 69 | }); 70 | continue; 71 | } 72 | 73 | switch (fields[k].type) { 74 | case "bool": 75 | case "boolean": 76 | data[k] = (data[k] == 1 || data[k] === true); 77 | break; 78 | case "struct": 79 | case "object": 80 | if (typeof data[k] == "string") { 81 | try { 82 | data[k] = (data[k].length > 0 ? JSON.parse(data[k]) : {}); 83 | } catch (e) { 84 | data[k] = {}; 85 | } 86 | } 87 | break; 88 | default: 89 | if (typeof fields[k].type == "function") { 90 | try { 91 | data[k] = new (fields[k].type)(data[k]); 92 | } catch (e) { 93 | data[k] = undefined; 94 | } 95 | } 96 | } 97 | 98 | this[k] = data[k]; 99 | } 100 | 101 | for (var i = 0; i < associations.length; i++) { 102 | switch (associations[i].type) { 103 | case "one": 104 | if (!this.hasOwnProperty(associations[i].field + "_id")) { 105 | if (data.hasOwnProperty(associations[i].field)) { 106 | this[associations[i].field + "_id"] = data[associations[i].field].id; 107 | } else if (!data.hasOwnProperty(associations[i].field + "_id")) { 108 | this[associations[i].field + "_id"] = 0; 109 | } else { 110 | this[associations[i].field + "_id"] = data[associations[i].field + "_id"]; 111 | } 112 | } 113 | associationHelpers.one.fetch(this, associations[i], opts); 114 | break; 115 | case "many": 116 | associationHelpers.many.fetch(this, associations[i], opts); 117 | break; 118 | } 119 | } 120 | 121 | if (colParams && colParams.methods) { 122 | for (k in colParams.methods) { 123 | this[k] = data[k] = colParams.methods[k]; 124 | } 125 | } 126 | 127 | this._dataHash = require("./hash").hash(data); 128 | 129 | if (this._dataPending === 0) { 130 | this.emit("ready", this); 131 | } 132 | }; 133 | // this adds events to instances 134 | util.inherits(Model, events.EventEmitter); 135 | 136 | associationHelpers.one = require("./associations/one").define(orm, Model, model, fields, colParams, plugins); 137 | associationHelpers.many = require("./associations/many").define(orm, Model, model, fields, colParams, plugins); 138 | 139 | // this adds events to object 140 | require("./events").extend(Model); 141 | 142 | Model.prototype._getData = function () { 143 | var data = {}; 144 | 145 | if (this.hasOwnProperty(idProperty)) { 146 | data[idProperty] = this[idProperty]; 147 | } 148 | 149 | for (var k in fields) { 150 | if (!fields.hasOwnProperty(k)) continue; 151 | 152 | // don't set default values, if property is not set, ignore it 153 | if (!this.hasOwnProperty(k)) continue; 154 | 155 | switch (fields[k].type) { 156 | case "bool": 157 | case "boolean": 158 | data[k] = (this[k] == 1); 159 | break; 160 | case "struct": 161 | case "object": 162 | if (this[k]) { 163 | data[k] = (typeof this[k] == "object" ? JSON.stringify(this[k]) : this[k]); 164 | } else { 165 | data[k] = ""; 166 | } 167 | break; 168 | default: 169 | data[k] = (this[k] && this[k].hasOwnProperty("toString") ? this[k].toString() : this[k]); 170 | } 171 | } 172 | 173 | for (var i = 0; i < associations.length; i++) { 174 | if (associations[i].type == "one") { 175 | if (this.hasOwnProperty(associations[i].field + "_id")) { 176 | data[associations[i].field + "_id"] = this[associations[i].field + "_id"]; 177 | } 178 | } 179 | } 180 | 181 | return data; 182 | }; 183 | Model.prototype.ready = function (cb) { 184 | if (typeof cb == "function") { 185 | if (this._dataPending === 0) { 186 | cb(this); 187 | } else { 188 | this.once("ready", cb); 189 | } 190 | 191 | return this; 192 | } 193 | 194 | return (this._pending === 0); 195 | }; 196 | Model.prototype.saved = function () { 197 | return (this._dataHash == require("./hash").hash(this._getData())); 198 | }; 199 | Model.prototype.created = function () { 200 | return this.hasOwnProperty(idProperty); 201 | }; 202 | Model.prototype.save = function () { 203 | var callback = function () {}, opts = {}, data = {}, self = this; 204 | var validators = []; 205 | var saveRecord = function () { 206 | orm._db.saveRecord(idProperty, model, data, function (err, id) { 207 | if (err) { 208 | if (colParams && colParams.hooks && typeof colParams.hooks.afterSave == "function") { 209 | colParams.hooks.afterSave(false, self); 210 | } 211 | if (typeof callback == "function") { 212 | callback(err); 213 | } 214 | return; 215 | } 216 | 217 | if (!self[idProperty]) self[idProperty] = id; 218 | 219 | if (colParams && colParams.hooks && typeof colParams.hooks.afterSave == "function") { 220 | colParams.hooks.afterSave(true, self); 221 | } 222 | 223 | if (plugins.hasOwnProperty("afterSave")) { 224 | for (var k in plugins["afterSave"]) { 225 | plugins["afterSave"][k](data, model); 226 | } 227 | } 228 | 229 | if (typeof callback == "function") { 230 | callback(null, self); 231 | } 232 | }); 233 | }; 234 | 235 | for (var i = 0; i < arguments.length; i++) { 236 | if (typeof arguments[i] == "function") { 237 | callback = arguments[i]; 238 | } else if (typeof arguments[i] == "object") { 239 | opts = arguments[i] || {}; // avoid null (yes, it's an object) 240 | } 241 | } 242 | 243 | data = this._getData(); 244 | 245 | if (colParams && colParams.validations) { 246 | for (var field in colParams.validations) { 247 | if (!colParams.validations.hasOwnProperty(field) || !data.hasOwnProperty(field)) continue; 248 | 249 | if (typeof colParams.validations[field] === "function") { 250 | validators.push([ field, data[field], colParams.validations[field] ]); 251 | } else if (Array.isArray(colParams.validations[field])) { 252 | for (i = 0; i < colParams.validations[field].length; i++) { 253 | validators.push([ field, data[field], colParams.validations[field][i] ]); 254 | } 255 | } 256 | } 257 | } 258 | 259 | for (var k in fields) { 260 | if (!fields.hasOwnProperty(k)) continue; 261 | if (!fields[k].hasOwnProperty("validations")) continue; 262 | 263 | if (typeof fields[k].validations === "function") { 264 | validators.push([ k, data[k], fields[k].validations ]); 265 | } else if (Array.isArray(fields[k].validations)) { 266 | for (i = 0; i < fields[k].validations.length; i++) { 267 | validators.push([ k, data[k], fields[k].validations[i] ]); 268 | } 269 | } 270 | } 271 | 272 | if (validators.length > 0) { 273 | var validatorIndex = 0, errors = []; 274 | var validatorNext = function (response) { 275 | if (typeof response == "undefined" || response === true) { 276 | validatorIndex++; 277 | return callNextValidator(); 278 | } 279 | errors.push({ 280 | "type": "validator", 281 | "field": validators[validatorIndex][0], 282 | "value": validators[validatorIndex][1], 283 | "msg": response 284 | }); 285 | if (opts.validateAll) { 286 | validatorIndex++; 287 | return callNextValidator(); 288 | } 289 | return callback(errors[0]); 290 | }; 291 | var callNextValidator = function () { 292 | if (validatorIndex >= validators.length) { 293 | if (opts.validateAll && errors.length > 0) { 294 | return callback(errors); 295 | } 296 | 297 | if (colParams && colParams.hooks && typeof colParams.hooks.beforeSave == "function") { 298 | colParams.hooks.beforeSave(self); 299 | } 300 | return saveRecord(); 301 | } 302 | validators[validatorIndex][2].apply(self, [ validators[validatorIndex][1], validatorNext, data, Model, validators[validatorIndex][0] ]); 303 | }; 304 | return callNextValidator(); 305 | } 306 | 307 | if (colParams && colParams.hooks && typeof colParams.hooks.beforeSave == "function") { 308 | colParams.hooks.beforeSave(self); 309 | } 310 | 311 | // convert date fields 312 | for (k in fields) { 313 | if (!fields.hasOwnProperty(k)) continue; 314 | if (!this.hasOwnProperty(k)) continue; 315 | 316 | if (fields[k].type == "date") { 317 | if (util.isDate(this[k])) { 318 | data[k] = moment(this[k]).format("YYYY-MM-DD HH:mm:ss"); 319 | } else { 320 | data[k] = null; 321 | } 322 | } 323 | } 324 | 325 | return saveRecord(); 326 | }; 327 | Model.prototype.remove = function (callback) { 328 | if (this.hasOwnProperty(idProperty)) { 329 | var self = this; 330 | var conditions = {}; 331 | 332 | conditions[idProperty] = this[idProperty]; 333 | 334 | orm._db.clearRecords(model, { 335 | "conditions": conditions, 336 | "callback": function (err, info) { 337 | /* 338 | The object will still have all properties and you can save() later 339 | if you want (a new ID should be assigned) 340 | */ 341 | delete self[idProperty]; 342 | 343 | if (typeof callback == "function") callback(!err); 344 | } 345 | }); 346 | return; 347 | } 348 | // no id so nothing to "unsave" 349 | if (typeof callback == "function") callback(true); 350 | }; 351 | 352 | associationHelpers.one.extend(associations); 353 | associationHelpers.many.extend(associations); 354 | 355 | Model.plugin = function (plugin) { 356 | var calls = require("./plugins/" + plugin); 357 | 358 | for (var k in calls) { 359 | if (!plugins.hasOwnProperty(k)) { 360 | plugins[k] = []; 361 | } 362 | plugins[k].push(calls[k]); 363 | } 364 | 365 | return this; 366 | }; 367 | Model.sync = function (opts, callback) { 368 | for (var i = 0; i < associations.length; i++) { 369 | if (!associations[i].hasOwnProperty("opts") || !associations[i].opts.hasOwnProperty("properties")) continue; 370 | 371 | for (var k in associations[i].opts.properties) { 372 | if (!associations[i].opts.properties.hasOwnProperty(k)) { 373 | continue; 374 | } 375 | if (typeof associations[i].opts.properties[k] == "function") { 376 | var o = new associations[i].opts.properties[k](); 377 | if (o instanceof String || o instanceof Boolean || o instanceof Number) { 378 | associations[i].opts.properties[k] = { "type": typeof associations[i].opts.properties[k]() }; 379 | } else if (o instanceof Date) { 380 | associations[i].opts.properties[k] = { "type": "date" }; 381 | } else { 382 | associations[i].opts.properties[k] = { "type": associations[i].opts.properties[k] }; 383 | } 384 | } else if (typeof associations[i].opts.properties[k] == "string") { 385 | associations[i].opts.properties[k] = { "type": associations[i].opts.properties[k].toLowerCase() }; 386 | } 387 | } 388 | } 389 | orm._db.createCollection(model, fields, associations, opts, callback); 390 | }; 391 | Model.clear = function (callback) { 392 | orm._db.clearRecords(model, { 393 | "callback": function (err, info) { 394 | if (typeof callback == "function") callback(!err); 395 | } 396 | }); 397 | }; 398 | Model.get = function (id, callback) { 399 | var modelOpts = {}; 400 | var conditions = {}; 401 | 402 | if (colParams.hasOwnProperty("fetchDepth")) { 403 | modelOpts.fetchDepth = colParams.fetchDepth; 404 | } 405 | 406 | conditions[idProperty] = id; 407 | 408 | orm._db.selectRecords(model, { 409 | "conditions": conditions, 410 | "callback" : function (err, data) { 411 | if (err || data.length === 0) return callback(null); 412 | 413 | (new Model(data[0], modelOpts)).ready(callback); 414 | } 415 | }); 416 | }; 417 | Model.find = function () { 418 | var args = arguments; 419 | var callback = null; 420 | var config = { rel: [] }; 421 | var last_arg = arguments.length - 1; 422 | var modelOpts = {}; 423 | 424 | if (last_arg >= 0 && typeof arguments[last_arg] == "function") { 425 | callback = arguments[last_arg]; 426 | last_arg--; 427 | } 428 | 429 | //.find(callback); 430 | //.find(conditions, callback); 431 | //.find(conditions, limit, callback); 432 | //.find(conditions, order, callback); 433 | //.find(conditions, order, limit, callback); 434 | 435 | for (var i = 0; i <= last_arg; i++) { 436 | switch (typeof arguments[i]) { 437 | case "object": // conditions 438 | config.conditions = arguments[i]; 439 | 440 | for (var j = 0; j < associations.length; j++) { 441 | if (associations[j].type == "one" && config.conditions.hasOwnProperty(associations[j].field)) { 442 | config.conditions[associations[j].field + "_id"] = config.conditions[associations[j].field]; 443 | delete config.conditions[associations[j].field]; 444 | } else if (associations[j].type == "many" && config.conditions.hasOwnProperty(associations[j].name)) { 445 | config.rel.push({ 446 | collection: model + "_" + associations[j].field, 447 | rel: [ "id", model + "_id", associations[j].name + "_id" ], 448 | value: config.conditions[associations[j].name].id || config.conditions[associations[j].name] 449 | }); 450 | delete config.conditions[associations[j].name]; 451 | } 452 | } 453 | break; 454 | case "number": // limit 455 | config.limit = arguments[i]; 456 | break; 457 | case "string": // order 458 | config.order = arguments[i]; 459 | break; 460 | } 461 | } 462 | 463 | if (colParams.hasOwnProperty("fetchDepth")) { 464 | modelOpts.fetchDepth = colParams.fetchDepth; 465 | } 466 | 467 | if (callback !== null) { 468 | config.callback = function (err, data) { 469 | if (err || data.length === 0) return callback(null); 470 | 471 | var pending = data.length; 472 | var checkReady = function () { 473 | if (pending-- == 1) { 474 | callback(data); 475 | } 476 | }; 477 | 478 | for (var i = 0; i < data.length; i++) { 479 | data[i] = new Model(data[i], modelOpts); 480 | 481 | data[i].ready(checkReady); 482 | } 483 | }; 484 | orm._db.selectRecords(model, config); 485 | 486 | return this; 487 | } else { 488 | var query = orm._db.selectRecords(model, config); 489 | 490 | (function (m) { 491 | query.on("record", function (record) { 492 | (new Model(record, modelOpts)).ready(function (m) { 493 | m.emit("record", m); 494 | }); 495 | }); 496 | query.on("end", function (info) { 497 | // info not properly processed yet 498 | m.emit("end"); 499 | }); 500 | query.on("error", function (err) { 501 | m.emit("error", err); 502 | }); 503 | })(this); 504 | 505 | return query; 506 | } 507 | }; 508 | Model.textsearch = function () { 509 | var args = arguments; 510 | var callback = null; 511 | var config = {}; 512 | var last_arg = arguments.length - 1; 513 | 514 | if (last_arg >= 0) { 515 | callback = arguments[last_arg]; 516 | last_arg--; 517 | } 518 | 519 | if (!orm._db.searchRecords) { 520 | return callback(null); 521 | } 522 | 523 | //.textsearch(text, callback); 524 | //.textsearch(text, limit, callback); 525 | //.textsearch(limit, text, callback); 526 | 527 | for (var i = 0; i <= last_arg; i++) { 528 | switch (typeof arguments[i]) { 529 | case "number": // limit 530 | config.limit = arguments[i]; 531 | break; 532 | case "string": // text 533 | config.text = arguments[i]; 534 | break; 535 | } 536 | } 537 | 538 | if (!config.text) return callback(null); 539 | 540 | if (callback !== null) { 541 | config.callback = function (err, data) { 542 | if (err || data.length === 0) return callback(null); 543 | 544 | for (var i = 0; i < data.length; i++) { 545 | data[i] = new Model(data[i]); 546 | } 547 | callback(data); 548 | }; 549 | } 550 | orm._db.searchRecords(model, config); 551 | }; 552 | Model._ORM = { "collection": model }; 553 | 554 | colParams || (colParams = {}); 555 | 556 | for (var k in fields) { 557 | if (!fields.hasOwnProperty(k)) { 558 | continue; 559 | } 560 | if (typeof fields[k] == "function") { 561 | var o = new fields[k](); 562 | if (o instanceof String || o instanceof Boolean || o instanceof Number) { 563 | fields[k] = { "type": typeof fields[k]() }; 564 | } else if (o instanceof Date) { 565 | fields[k] = { "type": "date" }; 566 | } else if (fields[k]._ORM) { 567 | Model.hasOne(k, fields[k], { autoFetch: true }); 568 | delete fields[k]; 569 | } else { 570 | fields[k] = { "type": fields[k] }; 571 | } 572 | } else if (typeof fields[k] == "string") { 573 | fields[k] = { "type": fields[k].toLowerCase() }; 574 | } else if (Array.isArray(fields[k]) && fields[k].length > 0 && fields[k][0].hasMany) { 575 | Model.hasMany(k + (k.substr(-1) != "s" ? "s" : ""), fields[k][0], k); 576 | delete fields[k]; 577 | } 578 | } 579 | 580 | this._models[model] = Model; 581 | if (!module.exports.hasOwnProperty(model)) { 582 | module.exports[model] = this._models[model]; 583 | } 584 | 585 | return this._models[model]; 586 | }; 587 | 588 | module.exports = { 589 | "validators": require("./validators"), 590 | "F": function (str, value) { 591 | return { __ormFunction: str, v: value }; 592 | }, 593 | "connect" : function () { 594 | var dbObject = null; 595 | var dbType = null; 596 | var dbPath = ""; 597 | var cb = function () {}; 598 | var uri = ""; 599 | var exists = process.version.match(/^v0\.6\./) ? require("path").exists : require("fs").exists; 600 | 601 | for (var i = 0; i < arguments.length; i++) { 602 | switch (typeof arguments[i]) { 603 | case "string": 604 | uri = dbType = arguments[i]; 605 | break; 606 | case "function": 607 | cb = arguments[i]; 608 | break; 609 | case "object": 610 | dbObject = arguments[i]; 611 | break; 612 | } 613 | } 614 | 615 | if (dbObject === null) { 616 | uri = require("url").parse(uri); 617 | 618 | if (!uri.protocol) { 619 | return cb(false, { "number": 1, "message": "Protocol not defined" }); 620 | } 621 | 622 | dbType = uri.protocol.substr(0, uri.protocol.length - 1); 623 | } 624 | 625 | if (db_alias.hasOwnProperty(dbType)) { 626 | dbType = db_alias[dbType]; 627 | } 628 | 629 | dbPath = __dirname + "/databases/" + dbType + ".js"; 630 | 631 | exists(dbPath, function (exists) { 632 | if (!exists) { 633 | return cb(false, { "number": 2, "message": "Protocol not installed" }); 634 | } 635 | 636 | var db = require(dbPath); 637 | 638 | var handleResult = function (success, info) { 639 | if (!success) return cb(false, info); 640 | 641 | return cb(true, new ORM(info)); 642 | }; 643 | 644 | if (dbObject !== null) { 645 | db.use_db(dbObject, handleResult); 646 | } else { 647 | db.connect(uri, handleResult); 648 | } 649 | }); 650 | } 651 | }; 652 | -------------------------------------------------------------------------------- /lib/plugins/debug.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "afterSave": function (data, Model) { 3 | console.log("record saved", data, Model._ORM); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a supposed to be a list of common validators 3 | * that can be reused instead of creating new ones. 4 | **/ 5 | var validators = {}; 6 | 7 | /** 8 | * Check if a number is between a minimum and 9 | * a maximum number. One of this constraints 10 | * can be omitted. 11 | **/ 12 | validators.rangeNumber = function (min, max) { 13 | return function (n, next) { 14 | if (min === undefined && n <= max) return next(); 15 | if (max === undefined && n >= min) return next(); 16 | if (n >= min && n <= max) return next(); 17 | return next('out-of-range-number'); 18 | }; 19 | }; 20 | 21 | /** 22 | * Check if a string length is between a minimum 23 | * and a maximum number. One of this constraints 24 | * can be omitted. 25 | **/ 26 | validators.rangeLength = function (min, max) { 27 | return function (v, next) { 28 | if (v === undefined) return next('undefined'); 29 | if (min === undefined && v.length <= max) return next(); 30 | if (max === undefined && v.length >= min) return next(); 31 | if (v.length >= min && v.length <= max) return next(); 32 | return next('out-of-range-length'); 33 | }; 34 | }; 35 | 36 | /** 37 | * Check if a value (number or string) is 38 | * in a list of values. 39 | **/ 40 | validators.insideList = function (list) { 41 | return function (v, next) { 42 | if (list.indexOf(v) >= 0) return next(); 43 | return next('outside-list'); 44 | }; 45 | }; 46 | 47 | /** 48 | * Check if a value (number or string) is 49 | * not in a list of values. 50 | **/ 51 | validators.outsideList = function (list) { 52 | return function (v, next) { 53 | if (list.indexOf(v) == -1) return next(); 54 | return next('inside-list'); 55 | }; 56 | }; 57 | 58 | /** 59 | * Check if a value is the same as a value 60 | * of another property (useful for password 61 | * checking). 62 | **/ 63 | validators.equalToProperty = function (name) { 64 | return function (v, next, data) { 65 | // could also do: v == this[name] 66 | if (v == data[name]) return next(); 67 | return next('not-equal-to-property'); 68 | }; 69 | }; 70 | 71 | /** 72 | * Check if a string has zero length. Sometimes 73 | * you might want to have a property on your 74 | * model that is not required but on a specific 75 | * form it can be. 76 | **/ 77 | validators.notEmptyString = function () { 78 | return validators.rangeLength(1); 79 | }; 80 | 81 | /** 82 | * Check if a property is unique in the collection. 83 | * This can take a while because a query has to be 84 | * made against the Model, but if you use this 85 | * always you should not have not unique values 86 | * on this property so this should not worry you. 87 | **/ 88 | validators.unique = function () { 89 | return function (v, next, data, Model, prop) { 90 | var query = {}; 91 | query[prop] = v; 92 | 93 | Model.find(query, function (records) { 94 | if (!records || records.length === 0) { 95 | return next(); 96 | } 97 | if (records.length == 1 && records[0].id === data.id) { 98 | return next(); 99 | } 100 | return next("not-unique"); 101 | }); 102 | }; 103 | }; 104 | 105 | /** 106 | * Pattern validators are usually based on regular 107 | * expressions and solve more complicated validations 108 | * you might need. 109 | **/ 110 | validators.patterns = {}; 111 | 112 | /** 113 | * Check if a value matches a given pattern. 114 | * You can define a pattern string and regex 115 | * modifiers or just send the RegExp object 116 | * as 1st argument. 117 | **/ 118 | validators.patterns.match = function (pattern, modifiers) { 119 | return function (v, next) { 120 | if (typeof pattern == "string") { 121 | pattern = new RegExp(pattern, modifiers); 122 | } 123 | if (typeof v == "string" && v.match(pattern)) return next(); 124 | return next('no-pattern-match'); 125 | }; 126 | }; 127 | 128 | /** 129 | * Check if a value is an hexadecimal string 130 | * (letters from A to F and numbers). 131 | **/ 132 | validators.patterns.hexString = function () { 133 | return validators.patterns.match("^[a-f0-9]+$", "i"); 134 | }; 135 | 136 | /** 137 | * Check if a value is an e-mail address 138 | * (simple checking, works 99%). 139 | **/ 140 | validators.patterns.email = function () { 141 | return validators.patterns.match("^[a-z0-9\\._%\\+\\-]+@[a-z0-9\\.\\-]+\\.[a-z]{2,6}$", "i"); 142 | }; 143 | 144 | /** 145 | * Check if it's a valid IPv4 address. 146 | **/ 147 | validators.patterns.ipv4 = function () { 148 | var part = "(1[0-9]{,2}|2[0-4][0-9]|25[0-4])"; 149 | return validators.patterns.match("^" + [ part, part, part, part ].join("\\.") + "$", ""); 150 | }; 151 | 152 | module.exports = validators; 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "orm", 3 | "version" : "0.1.8-12", 4 | "description" : "NodeJS Object-relational mapping", 5 | "keywords" : [ 6 | "orm", 7 | "mysql", 8 | "postgresql", 9 | "mongodb", 10 | "database", 11 | "relational" 12 | ], 13 | "author" : "Diogo Resende ", 14 | "contributors": [ 15 | { "name": "Michael Axiak", "email": "mike@axiak.net" }, 16 | { "name": "Michael Yagudaev", "email": "michael@yagudaev.com" }, 17 | { "name": "booo" }, 18 | { "name": "shaneiseminger" }, 19 | { "name": "shine-on" } 20 | ], 21 | "engines" : { 22 | "node" : ">= 0.4.0" 23 | }, 24 | "dependencies": { 25 | "moment": "1.6.x" 26 | }, 27 | "repository" : { 28 | "type" : "git", 29 | "url" : "http://github.com/dresende/node-orm.git" 30 | }, 31 | "directories": { 32 | "lib" : "./lib", 33 | "example": "./examples" 34 | }, 35 | "main" : "./lib/orm", 36 | "licenses" : [{ 37 | "type" : "MIT", 38 | "url" : "http://github.com/dresende/node-orm/raw/master/LICENSE" 39 | }] 40 | } 41 | -------------------------------------------------------------------------------- /test/database-helpers.js: -------------------------------------------------------------------------------- 1 | var helpers = require("../lib/databases/helpers"); 2 | var should = require("should"); 3 | var sqlWhereOpts = { 4 | boolean_convert: function (val) { 5 | return val ? 'pass' : 'fail'; 6 | } 7 | }; 8 | 9 | describe("helpers", function () { 10 | var tests = { 11 | buildSqlOrder: [ 12 | [ [ "A" ], " ORDER BY A ASC" ], 13 | [ [ "A Asc" ], " ORDER BY A ASC" ], 14 | [ [ "A Desc" ], " ORDER BY A DESC" ], 15 | [ [ "A B Asc" ], " ORDER BY A ASC, B ASC" ], 16 | [ [ "A Asc B Asc" ], " ORDER BY A ASC, B ASC" ], 17 | [ [ "A Desc B" ], " ORDER BY A DESC, B ASC" ], 18 | [ [ " A B C " ], " ORDER BY A ASC, B ASC, C ASC" ], 19 | [ [ "A" ], " ORDER BY A ASC" ] 20 | ], 21 | buildSqlLimit: [ 22 | [ [ "N" ], " LIMIT N" ], 23 | [ [ "N", "M" ], " LIMIT M, N" ] 24 | ], 25 | buildSqlWhere: [ 26 | [ [ {}, escapeCb ], [ " WHERE ", [] ] ], 27 | [ [ { "prop1": 2 }, escapeCb ], [ " WHERE $prop1$=?", [ 2 ] ] ], 28 | [ [ { "prop1": 'a' }, escapeCb ], [ " WHERE $prop1$=?", [ 'a' ] ] ], 29 | [ [ { "prop1": 2, "prop2": '2' }, escapeCb ], [ " WHERE $prop1$=? AND $prop2$=?", [ 2, '2' ] ] ], 30 | [ [ { "prop1": 2, "prop2": true }, escapeCb, sqlWhereOpts ], [ " WHERE $prop1$=? AND $prop2$=?", [ 2, 'pass' ] ] ], 31 | [ [ { "prop1": 2, "prop2": false }, escapeCb, sqlWhereOpts ], [ " WHERE $prop1$=? AND $prop2$=?", [ 2, 'fail' ] ] ], 32 | [ [ { "prop1 !": 2 }, escapeCb ], [ " WHERE $prop1$!=?", [ 2 ] ] ], 33 | [ [ { "prop1 !=": 2 }, escapeCb ], [ " WHERE $prop1$!=?", [ 2 ] ] ], 34 | [ [ { "prop1 >": 2 }, escapeCb ], [ " WHERE $prop1$>?", [ 2 ] ] ], 35 | [ [ { "prop1 <": 2 }, escapeCb ], [ " WHERE $prop1$=": 2 }, escapeCb ], [ " WHERE $prop1$>=?", [ 2 ] ] ], 37 | [ [ { "prop1 <=": 2 }, escapeCb ], [ " WHERE $prop1$<=?", [ 2 ] ] ], 38 | [ [ { "prop1": [ ] }, escapeCb ], [ " WHERE $prop1$ IN (NULL)", [ ] ] ], 39 | [ [ { "prop1": [ 1, 2 ] }, escapeCb ], [ " WHERE $prop1$ IN (?,?)", [ 1, 2 ] ] ], 40 | [ [ { "prop1 !": [ 1, 2 ] }, escapeCb ], [ " WHERE $prop1$ NOT IN (?,?)", [ 1, 2 ] ] ] 41 | ] 42 | }; 43 | 44 | for (var k in tests) { 45 | for (var i = 0; i < tests[k].length; i++) { 46 | addTest(k, tests[k][i][0], tests[k][i][1]); 47 | } 48 | } 49 | }); 50 | 51 | function addTest(fun, params, expected) { 52 | var p = []; 53 | for (var i = 0; i < params.length; i++) { 54 | switch (typeof params[i]) { 55 | case "object": 56 | if (Array.isArray(params[i])) { 57 | p.push("[" + params[i].join(",") + "]"); 58 | } else { 59 | p.push(params[i]); 60 | } 61 | break; 62 | case "function": 63 | p.push("[Function]"); 64 | break; 65 | case "string": 66 | p.push("'" + params[i] + "'"); 67 | break; 68 | default: 69 | p.push(params[i]); 70 | } 71 | } 72 | 73 | describe("." + fun + "(" + p.join(", ") + ")", function () { 74 | it("should return '" + expected + "'", function () { 75 | helpers[fun].apply(helpers, params).should.eql(expected); 76 | }); 77 | }); 78 | } 79 | 80 | function escapeCb(name) { 81 | return '$' + name + '$'; 82 | } 83 | -------------------------------------------------------------------------------- /test/validators.js: -------------------------------------------------------------------------------- 1 | var validators = require("../lib/validators"); 2 | var should = require("should"); 3 | 4 | describe("validators", function () { 5 | var tests = { 6 | rangeNumber: [ 7 | [ [ 3, 6 ], [ 3 ] ], 8 | [ [ 3, 6 ], [ 4 ] ], 9 | [ [ 3, 6 ], [ 6 ] ], 10 | [ [ 3, 6 ], [ -4 ], "out-of-range-number" ], 11 | [ [ 3 ], [ 3 ] ], 12 | [ [ 3 ], [ 5 ] ], 13 | [ [ 3 ], [ 2 ], "out-of-range-number" ], 14 | [ [ 3 ], [ -2 ], "out-of-range-number" ], 15 | [ [ undefined, 3 ], [ -2 ] ], 16 | [ [ undefined, 3 ], [ 2 ] ], 17 | [ [ undefined, 3 ], [ 3 ] ], 18 | [ [ undefined, 3 ], [ 5 ], "out-of-range-number" ] 19 | ], 20 | rangeLength: [ 21 | [ [ 3, 6 ], [ "abc" ] ], 22 | [ [ 3, 6 ], [ "abcdef" ] ], 23 | [ [ 3, 6 ], [ "ab" ], "out-of-range-length" ], 24 | [ [ 3, 6 ], [ "abcdefg" ], "out-of-range-length" ], 25 | [ [ 3, 6 ], [ undefined ], "undefined" ] 26 | ], 27 | insideList: [ 28 | [ [[ 1, 2, "a" ]], [ 1 ] ], 29 | [ [[ 1, 2, "a" ]], [ 2 ] ], 30 | [ [[ 1, 2, "a" ]], [ "a" ] ], 31 | [ [[ 1, 2, "a" ]], [ "1" ], "outside-list" ] 32 | ], 33 | outsideList: [ 34 | [ [[ 1, 2, "a" ]], [ "1" ] ], 35 | [ [[ 1, 2, "a" ]], [ 1 ], "inside-list" ], 36 | [ [[ 1, 2, "a" ]], [ 2 ], "inside-list" ], 37 | [ [[ 1, 2, "a" ]], [ "a" ], "inside-list" ] 38 | ], 39 | equalToProperty: [ 40 | [ [ "strprop" ], [ "str" ] ], 41 | [ [ "strprop" ], [ "not" ], "not-equal-to-property" ], 42 | [ [ "numprop" ], [ 2 ] ], 43 | [ [ "numprop" ], [ "2" ] ], 44 | [ [ "numprop" ], [ 3 ], "not-equal-to-property" ], 45 | [ [ "boolprop" ], [ false ] ], 46 | [ [ "boolprop" ], [ null ], "not-equal-to-property" ], 47 | [ [ "boolprop" ], [ true ], "not-equal-to-property" ], 48 | [ [ "boolprop" ], [ undefined ], "not-equal-to-property" ] 49 | ] 50 | }; 51 | for (var k in tests) { 52 | for (var i = 0; i < tests[k].length; i++) { 53 | addTest(k, tests[k][i][0], tests[k][i][1], tests[k][i][2]); 54 | } 55 | } 56 | }); 57 | 58 | function addTest(fun, params1, params2, throwValue) { 59 | var test = testName(fun, params1, params2); 60 | 61 | if (!throwValue) { 62 | return describe(test, shouldNotThrow(validators[fun], params1, params2)); 63 | } 64 | return describe(test, shouldThrow(validators[fun], params1, params2, throwValue)); 65 | } 66 | 67 | function testName(fun, params1, params2) { 68 | return "." + fun + "(" + params1.join(", ") + ")(" + params2.join(", ") + ")"; 69 | } 70 | 71 | function shouldNotThrow(validator, build_params, call_params) { 72 | call_params.push(function (err) { 73 | should.strictEqual(undefined, err); 74 | }); 75 | call_params.push({ strprop: "str", numprop: 2, boolprop: false }); 76 | 77 | return function () { 78 | it("should not throw error", function () { 79 | validator.apply(null, build_params).apply(null, call_params); 80 | }); 81 | }; 82 | } 83 | 84 | function shouldThrow(validator, build_params, call_params, error) { 85 | call_params.push(function (err) { 86 | should.strictEqual(error, err); 87 | }); 88 | call_params.push({ strprop: "str", numprop: 2, boolprop: false }); 89 | 90 | return function () { 91 | it("should throw error '" + error + "'", function () { 92 | validator.apply(null, build_params).apply(null, call_params); 93 | }); 94 | }; 95 | } 96 | --------------------------------------------------------------------------------