├── index.js ├── .gitignore ├── lib ├── resql.js ├── Relation.js ├── dialects.js ├── Column.js ├── types.js ├── defer.js ├── Row.js ├── Database.js ├── Query.js └── Table.js ├── test ├── query_sql.js ├── rest.select.js ├── example.js ├── index.html └── testdb.js ├── package.json └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/resql.js"); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | *.komodoproject 11 | 12 | pids 13 | logs 14 | results 15 | 16 | node_modules 17 | npm-debug.log -------------------------------------------------------------------------------- /lib/resql.js: -------------------------------------------------------------------------------- 1 | // ResQL API 2 | 3 | var api = require("./types.js"), 4 | Database = require("./Database.js"), 5 | defer = require("./defer.js"); 6 | 7 | api.connect = function (options) { 8 | return new Database(options); 9 | }; 10 | 11 | api.defer = defer; 12 | 13 | module.exports = api; -------------------------------------------------------------------------------- /test/query_sql.js: -------------------------------------------------------------------------------- 1 | var testDb = require("./testdb.js"), 2 | Query = require("../lib/Query.js"), 3 | dialect = require("../lib/dialects")["mysql"]; 4 | 5 | var query = new Query(); 6 | 7 | query.verb = "select"; 8 | query.table = "students"; 9 | query.joins = [ 10 | {table: "courses", column: ""} 11 | ] 12 | 13 | console.log("Query1": query.sql()); -------------------------------------------------------------------------------- /lib/Relation.js: -------------------------------------------------------------------------------- 1 | module.exports = Relation; 2 | 3 | function Relation(table, name, options) { 4 | this.table = nameOf(table); 5 | this.name = name; 6 | this.type = options; 7 | this.targetTable = nameOf(options.table); 8 | this.targetColumn = nameOf(options.column); 9 | this.targetRelation = options.relation? nameOf(options.relation): null; 10 | this.outbound = options.outbound? true: false; 11 | }; 12 | 13 | function nameOf(entity) { 14 | return (typeof entity === "object"? entity.name: entity); 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "aravindet (askabt.com)", 3 | "name": "resql", 4 | "description": "Generates a REST API for any Relational Schema", 5 | "version": "0.0.2", 6 | "homepage": "http://github.com/askabt/resql", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/askabt/resql.git" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": {}, 13 | "optionalDependencies": {}, 14 | "main": "index", 15 | "engines": { 16 | "node": "*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/dialects.js: -------------------------------------------------------------------------------- 1 | exports.mysql = { 2 | nameDelim: "`", 3 | valueDelim: "'", 4 | types: { 5 | "boolean": "BOOLEAN", 6 | "integer": "INTEGER", 7 | "float": "FLOAT", 8 | "string": "VARCHAR(255)", 9 | "text": "TEXT", 10 | "date": "DATETIME", 11 | "serial": "INTEGER AUTO_INCREMENT", 12 | "default": "TEXT" 13 | }, 14 | index: { 15 | "primary": "PRIMARY KEY", 16 | "unique": "UNIQUE KEY" 17 | } 18 | }; 19 | 20 | exports.postgresql = { 21 | nameDelim: "\"", 22 | valueDelim: "'", 23 | types: { 24 | "boolean": "BOOLEAN", 25 | "integer": "INTEGER", 26 | "float": "FLOAT", 27 | "string": "VARCHAR(255)", 28 | "text": "TEXT", 29 | "date": "TIMESTAMP", 30 | "serial": "SERIAL", 31 | "default": "TEXT" 32 | }, 33 | index: { 34 | "primary": "PRIMARY KEY", 35 | "unique": "UNIQUE KEY" 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /lib/Column.js: -------------------------------------------------------------------------------- 1 | var types = require('./types.js'); 2 | 3 | module.exports = Column; 4 | 5 | function Column(table, name, type) { 6 | this.table = table; 7 | this.name = name; 8 | this.type = type; 9 | }; 10 | 11 | Column.prototype.createSql = function() { 12 | var db = this.table.database, dialect = db.dialect, ftable, ftype; 13 | var sql = dialect.nameDelim + this.name + dialect.nameDelim + " "; 14 | 15 | function colDef(type) { 16 | return dialect.types[type.name] || dialect.types["default"]; 17 | } 18 | 19 | if(this.type.name == "foreign") { 20 | ftable = db.tables[this.type.table]; 21 | ftable.finalize(); 22 | 23 | ftype = ftable.schema.id.type; 24 | if(ftype.name == "serial") ftype = types.Integer(); 25 | 26 | sql += colDef(ftype); 27 | 28 | } else { 29 | sql += colDef(this.type); 30 | if(this.index) sql += " " + (dialect.index[this.index] || ""); 31 | } 32 | return sql; 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /test/rest.select.js: -------------------------------------------------------------------------------- 1 | var rs = require("../lib/resql"), 2 | http = require("http"), 3 | db = new rs.connect({ 4 | dialect: "mysql", 5 | host: "localhost", 6 | user: "askabt", 7 | password: "askabt", 8 | database: "askabt" 9 | }); 10 | 11 | var students = db.table('students', { 12 | name: rs.String, 13 | dob: rs.Date, 14 | }); 15 | 16 | var courses = db.table('courses', { 17 | id: rs.String, 18 | name: rs.String, 19 | desc: rs.Text, 20 | teacherId: rs.Foreign('teachers'), 21 | 22 | faculty: rs.Relation('teacherId') 23 | }); 24 | 25 | var teachers = db.table('teachers', { 26 | name: rs.String, 27 | joined: rs.Date, 28 | 29 | courses: rs.Relation('courses', 'teacherId') 30 | }); 31 | 32 | // db.sync(true); 33 | 34 | var handler = db.restHandler({}); 35 | var form = require("fs").readFileSync("index.html"); 36 | http.createServer(function(req, res) { 37 | if(req.url == "/") 38 | res.end(form); 39 | else 40 | handler(req, res); 41 | }).listen(9999); 42 | console.log("http://localhost:9999"); 43 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | var db = require('./testdb'); 2 | 3 | var courses = db.tables.courses, 4 | teachers = db.tables.teachers, 5 | students = db.tables.students; 6 | 7 | 8 | students.one( 9 | { id: 1 }, 10 | { embed: { courses: { teacher: true } } } 11 | ). 12 | then(function(data) { 13 | console.log(JSON.stringify(data, null, 2)); 14 | }); 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | //db.options.simulate = true; 45 | 46 | //db.create(); 47 | //db.putDummyData().then(null,function(reason){console.log(reason);}); 48 | 49 | 50 | // students.one({id: 123}).courses().then(); 51 | 52 | /* 53 | //process.exit(0); 54 | // db.create(true); 55 | 56 | courses.one({id: 1234}).teacher().courses().then(function(res) { 57 | console.log(res); 58 | }); 59 | 60 | // process.exit(); 61 | 62 | students.add({ 63 | name: "Aravind", 64 | dob: new Date("1984-10-01") 65 | }); 66 | 67 | courses.one({id: 1234}).teacher().then( 68 | function(result) { 69 | console.log("courses/teacher:", result); 70 | } 71 | ); 72 | /* 73 | teachers.one({id: 1234}).coursesTaught().then(function(result) { 74 | console.log("teachers/coursesTaught", result); 75 | }); 76 | 77 | 78 | /* */ 79 | db.db.end(); 80 | 81 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ResQL tests 4 | 5 |
6 | Name:
7 | Date of birth:
8 | 9 | 10 | 11 | 12 |
13 | 14 | 42 | 43 | -------------------------------------------------------------------------------- /test/testdb.js: -------------------------------------------------------------------------------- 1 | var rs = require("../lib/resql"), 2 | db = new rs.connect({ 3 | dialect: "mysql", 4 | host: "localhost", 5 | user: "askabt", 6 | password: "askabt", 7 | database: "test" 8 | }); 9 | 10 | db.table('students', { 11 | name: rs.String, 12 | dob: rs.Date, 13 | 14 | courseMembership: rs.Relation('studentsInCourses', 'studentId'), 15 | courses: rs.Relation('studentsInCourses', 'studentId', 'course') 16 | }); 17 | 18 | db.table('courses', { 19 | name: rs.String, 20 | teacherId: rs.Foreign('teachers'), 21 | 22 | students: rs.Relation('studentsInCourses', 'courseId', 'student') 23 | }); 24 | 25 | db.table('teachers', { 26 | name: rs.String, 27 | joined: rs.Date, 28 | 29 | courses: rs.Relation('courses', 'teacherId') 30 | }); 31 | 32 | db.table('studentsInCourses', { 33 | studentId: rs.Foreign('students'), 34 | courseId: rs.Foreign('courses') 35 | }); 36 | 37 | 38 | db.putDummyData = function() { 39 | db.tables.students.add({id: 1, name: "Student A", dob: "1984-10-01"}); 40 | db.tables.students.add({id: 2, name: "Student B", dob: "1984-06-15"}); 41 | 42 | db.tables.teachers.add({id: 1, name: "Teacher A", joined: "2011-03-20"}); 43 | db.tables.teachers.add({id: 2, name: "Teacher B", joined: "2012-08-14"}); 44 | 45 | db.tables.courses.add({id: 1, name: "Physics", teacherId: 1}); 46 | db.tables.courses.add({id: 2, name: "Chemistry", teacherId: 2}); 47 | 48 | db.tables.studentsInCourses.add({studentId: 1, courseId: 1}); 49 | db.tables.studentsInCourses.add({studentId: 1, courseId: 2}); 50 | return db.tables.studentsInCourses.add({studentId: 2, courseId: 1}); 51 | 52 | // MySQL will do all the queries in order so when the 53 | // last one finishes we can be sure everything before 54 | // is done, too. 55 | } 56 | 57 | module.exports = db; -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | var dialects = require('./dialects.js'); 2 | 3 | var Type = function(options) { 4 | for(i in options) this[i] = options[i]; 5 | } 6 | 7 | Type.prototype.index = function(indexType) { 8 | var clone = Object.create(this); 9 | if(indexType && indexType !== "primary" && indexType !== "unique") 10 | throw "Unknown index type."; 11 | clone.index = indexType || "index"; 12 | return clone; 13 | } 14 | 15 | Type.prototype.configure = function(options) { 16 | var clone = Object.create(this); 17 | clone.options = options; 18 | return clone; 19 | } 20 | 21 | Type.prototype.createSql = function(dialect) { 22 | var sql = dialect.types[this.name] || dialect.types["default"]; 23 | switch (this.index) { 24 | case "primary": 25 | sql += "PRIMARY KEY"; 26 | break; 27 | case "unique": 28 | sql += "UNIQUE KEY"; 29 | break; 30 | } 31 | return sql; 32 | } 33 | 34 | exports.Boolean = function() { return new Type({ name: "boolean" }) }; 35 | exports.Integer = function() { return new Type({ name: "integer" }) }; 36 | exports.Float = function() { return new Type({ name: "float" }) }; 37 | exports.String = function() { return new Type({ name: "string" }) }; 38 | exports.Text = function() { return new Type({ name: "text" }) }; 39 | exports.Date = function() { return new Type({ name: "date" }) }; 40 | exports.Serial = function() { return new Type({ name: "serial" }) }; 41 | exports.Foreign = function (table) { 42 | if(!table) throw "Can't create foreign key without table reference!"; 43 | table = (typeof table === "string"? table: table.name); 44 | return new Type({ name: "foreign", table: table }); 45 | }; 46 | 47 | exports.Relation = function() { 48 | var options; 49 | if(arguments.length == 1) options = { 50 | column: arguments[0], outbound: true 51 | }; 52 | else if(arguments[0]) options = { 53 | table: arguments[0], column: arguments[1], 54 | relation: arguments[2], outbound: false 55 | }; 56 | else options = { 57 | column: arguments[1], relation: arguments[2], outbound: true 58 | }; 59 | return new Type({ name: "relation", options: options }); 60 | } -------------------------------------------------------------------------------- /lib/defer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file comes from the documentation of Kris Kowal's Q library. 3 | */ 4 | 5 | var defer = function () { 6 | var pending = [], value; 7 | return { 8 | resolve: function (_value) { 9 | if (pending) { 10 | value = ref(_value); 11 | for (var i = 0, ii = pending.length; i < ii; i++) { 12 | value.then.apply(value, pending[i]); 13 | } 14 | pending = undefined; 15 | } 16 | }, 17 | promise: { 18 | then: function (_callback, _errback) { 19 | var result = defer(); 20 | _callback = _callback || function (value) { 21 | return value; 22 | }; 23 | _errback = _errback || function (reason) { 24 | return reject(reason); 25 | }; 26 | var callback = function (value) { 27 | result.resolve(_callback(value)); 28 | }; 29 | var errback = function (reason) { 30 | result.resolve(_errback(reason)); 31 | }; 32 | if (pending) { 33 | pending.push([callback, errback]); 34 | } else { 35 | process.nextTick(function () { 36 | value.then(callback, errback); 37 | }); 38 | } 39 | return result.promise; 40 | } 41 | } 42 | }; 43 | }; 44 | 45 | var ref = function (value) { 46 | if (value && value.then) 47 | return value; 48 | return { 49 | then: function (callback) { 50 | var result = defer(); 51 | process.nextTick(function () { 52 | result.resolve(callback(value)); 53 | }); 54 | return result.promise; 55 | } 56 | }; 57 | }; 58 | 59 | var reject = function (reason) { 60 | return { 61 | then: function (callback, errback) { 62 | var result = defer(); 63 | process.nextTick(function () { 64 | result.resolve(errback(reason)); 65 | }); 66 | return result.promise; 67 | } 68 | }; 69 | }; 70 | 71 | defer.reject = reject; 72 | module.exports = defer; -------------------------------------------------------------------------------- /lib/Row.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TODO in relation accessor methods: 3 | * - support for target relation 4 | * - apply filters and options 5 | * 6 | * 7 | */ 8 | 9 | 10 | var Relation = require("./Relation"), 11 | Column = require("./Column"), 12 | Query = require("./Query"), 13 | Table; 14 | 15 | module.exports = Row; 16 | 17 | function Row(table) { 18 | var schema = table.schema, 19 | db = table.query.database; 20 | 21 | this.query = new Query(db); 22 | this.query.table = table.name; 23 | 24 | this.proto = table.proto; 25 | 26 | for(i in schema) { 27 | this[i] = function(field) { 28 | if(field instanceof Relation) { 29 | if(field.outbound) { 30 | return makeOutboundAccessor(field, db.tables); 31 | } else { 32 | return makeInboundAccessor(field, db.tables); 33 | } 34 | } else { 35 | return makeColumnAccessor(field); 36 | } 37 | }(schema[i]); 38 | } 39 | 40 | Table = table.constructor; 41 | } 42 | 43 | function makeOutboundAccessor(field, tables) { 44 | return function (filters, options) { 45 | var res, q = this.query; 46 | 47 | if(q.filters[field.targetColumn] && ( 48 | !(q.filters[field.targetColumn] instanceof Array) || 49 | q.filters[field.targetColumn][0] == "=" 50 | )) { 51 | q = q.filters[field.targetColumn]; 52 | } else { 53 | q.options.columns = [field.targetColumn]; 54 | q.verb = "select"; 55 | } 56 | 57 | res = new Row(tables[field.targetTable]); 58 | res.query.verb = "select"; 59 | 60 | if(field.targetRelation) { 61 | res.query.filters = {id: q}; 62 | return res[field.targetRelation](filters, options); 63 | } else { 64 | res.query.filters = filters || {}; 65 | res.query.filters.id = q; 66 | return res; 67 | } 68 | } 69 | } 70 | 71 | function makeInboundAccessor(field, tables) { 72 | return function (filters, options) { 73 | var res, q = this.query; 74 | if(q.filters.id && ( 75 | !(q.filters.id instanceof Array) || 76 | q.filters.id[0] == "=" 77 | )) { 78 | q = this.query.filters.id; 79 | } else { 80 | q.options.columns = ["id"] 81 | q.verb = "select"; 82 | } 83 | 84 | res = new Table(tables[field.targetTable]); 85 | 86 | if(field.targetRelation) { 87 | res.query.filters = {}; 88 | res.query.filters[field.targetColumn] = q; 89 | return res.related(field.targetRelation, filters, options); 90 | } else { 91 | res.query.filters = filters || {}; 92 | res.query.filters[field.targetColumn] = q; 93 | res.query.verb = "select"; 94 | return res; 95 | } 96 | } 97 | } 98 | 99 | function makeColumnAccessor(field) { 100 | return function () { 101 | return this.query.execute().then(function(data) { 102 | return data[field[name]]; 103 | }); 104 | } 105 | } 106 | 107 | Row.prototype.then = function(callback, errback) { 108 | var self = this; 109 | this.query.verb = "select"; 110 | return this.query.execute().then(function(data) { 111 | var i, obj; 112 | obj = Object.create(self.proto); 113 | for(i in data[0]) { obj[i] = data[0][i]; } 114 | obj.save = function() { 115 | self.save(this); 116 | } 117 | callback(obj); 118 | }, 119 | errback); 120 | }; 121 | 122 | Row.prototype.save = function (data) { 123 | this.query.verb = "update"; 124 | this.query.data = data; 125 | 126 | return this.query.execute(); 127 | } 128 | 129 | /* 130 | List of unallowed column/relation names: 131 | - then 132 | - query 133 | - proto 134 | - save 135 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResQL 2 | 3 | ResQL is a Node.js ORM for working with MySQL an PostgreSQL databases. 4 | 5 | ## Contributing 6 | 7 | If you're in Bangalore and you're interested in Node.js, open source and getting 8 | paid for what you love to do, [Askabt](http://askabt.com) is looking to sponsor 9 | full- or part-time contributors to ResQL. Write to 10 | [askabt@askabt.in](mailto:askabt@askabt.in). 11 | 12 | ## Comparison with [Sequelize](http://github.com/sdepold/sequelize): 13 | 14 | Sequelize is currently the most popular Node.js ORM. 15 | 16 | Before going into the differences in detail, note that Sequelize is a 17 | fairly mature project and ResQL is not - right now you shouldn't use ResQL for 18 | production code. 19 | 20 | ResQL's _raison d'être_ is that it lets you code without nested callbacks. It 21 | accomplishes this by two techniques: 22 | - Function chaining to express complex chains of model queries, which are then 23 | combined using an SQL query builder before a DB call is made. 24 | - Using the [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) API 25 | to allow chaining of your own operations with model queries. 26 | 27 | Example: Consider the following code you might write using Sequelize: 28 | 29 | ```javascript 30 | students.find({where: {id: 34235}}).success(function(student) { 31 | student.getCourses().success(function(courses) { 32 | console.log(courses); 33 | }); 34 | }); 35 | ``` 36 | 37 | With ResQL, it's much neater: 38 | 39 | ```javascript 40 | students.one({id: 34235}).courses().then(function(courses) { 41 | console.log(courses); 42 | }); 43 | ``` 44 | 45 | ResQL is also more efficient as it makes a single optimized SQL query (using 46 | subqueries if necessary): 47 | 48 | ```sql 49 | SELECT * FROM "Courses" WHERE id IN ( 50 | SELECT courseId FROM "StudentCourses" WHERE userId='34235' 51 | ) 52 | ``` 53 | 54 | instead of 55 | 56 | ```sql 57 | SELECT * FROM "Students" WHERE "id"='34235' 58 | SELECT * FROM "StudentCourses" WHERE "userId"='34235' 59 | SELECT * FROM "Courses" WHERE "id" IN (...) 60 | ``` 61 | 62 | ResQL also provides an HTTP/REST API for reading and writing to the DB in the 63 | form of Connect/Express middleware. 64 | 65 | Another thing to note is that the data modeling APIs stay close to the actual 66 | table schema - you have complete control over foreign key names, for instance, 67 | and still get the benefits of the ORM's relation handling. A single intuitive 68 | relation() call replaces Sequelize's hasOne(), hasMany() and belongsTo(). 69 | 70 | To learn more, check out our [Guide](/askabt/resql/wiki/Guide) and 71 | [Module API](/askabt/resql/wiki/Module-API). 72 | 73 | License 74 | ------- 75 | (The MIT License) 76 | 77 | Copyright (c) 2009-2011 Askabt 78 | 79 | Permission is hereby granted, free of charge, to any person obtaining 80 | a copy of this software and associated documentation files (the 81 | 'Software'), to deal in the Software without restriction, including 82 | without limitation the rights to use, copy, modify, merge, publish, 83 | distribute, sublicense, and/or sell copies of the Software, and to 84 | permit persons to whom the Software is furnished to do so, subject to 85 | the following conditions: 86 | 87 | The above copyright notice and this permission notice shall be 88 | included in all copies or substantial portions of the Software. 89 | 90 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 91 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 92 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 93 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 94 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 95 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 96 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 97 | 98 | -------------------------------------------------------------------------------- /lib/Database.js: -------------------------------------------------------------------------------- 1 | var Table = require('./Table'), 2 | Row = require('./Row'), 3 | types = require('./types'), 4 | dialects = require('./dialects.js'); 5 | 6 | module.exports = Database; 7 | 8 | function Database(options) { 9 | var lib; 10 | 11 | this.options = options || {}; 12 | this.tables = {}; 13 | 14 | if(!options.dialect) options.dialect = "mysql"; 15 | if(!options.user) throw "No user specified."; 16 | if(!options.database) throw "No database specified."; 17 | 18 | this.dialect = dialects[options.dialect]; 19 | 20 | if(options.dialect == "mysql") { 21 | try { 22 | lib = require("mysql"); 23 | } catch(e) { 24 | throw "mysql library not found in node_modules"; 25 | } 26 | 27 | this.db = lib.createConnection(options); 28 | this.db.connect(function(err) { 29 | if(err) throw err; 30 | }); 31 | 32 | } else if(options.dialect == "postgresql") { 33 | try { 34 | lib = require("pg"); 35 | } catch(e) { 36 | throw "pg library not found in node_modules"; 37 | } 38 | 39 | this.db = lib.Client(options.path || options). 40 | connect(function(err) { 41 | if(err) throw err; 42 | }); 43 | console.log("PostgreSQL is not yet tested. It will probably not work correctly."); 44 | } else { 45 | throw "Unsupported SQL Dialect" 46 | } 47 | }; 48 | 49 | Database.prototype.table = function(name, schema) { 50 | return this.tables[name] = new Table(this, name, schema); 51 | }; 52 | 53 | Database.prototype.query = function (sql, callback) { 54 | console.log("Executing query: " + sql); 55 | 56 | if(!this.options.simulate) { 57 | this.db.query(sql, callback); 58 | } 59 | }; 60 | 61 | Database.prototype.create = function(force) { 62 | var i, sql; 63 | for(i in this.tables) { 64 | if(force){ 65 | console.log("Dropping table (if exists): " + i); 66 | if(!this.options.simulate) { 67 | this.db.query("DROP TABLE IF EXISTS " + i, 68 | function(err) { if(err) throw err; }); 69 | } 70 | } 71 | sql = this.tables[i].createSql(force); 72 | console.log("Creating table: " + sql); 73 | if(!this.options.simulate) { 74 | this.db.query(sql, function(err) { if(err) throw err; }); 75 | } 76 | } 77 | }; 78 | 79 | Database.prototype.restHandler = function(options) { 80 | var self = this, segpat = /^([^/]+)(\/|$)/; 81 | options = options || {}; 82 | 83 | return function(req, res, next) { 84 | var url = require("url").parse(req.url, true), 85 | path = url.pathname, 86 | params = url.query, 87 | data = ""; 88 | qo = self.tables; // query object 89 | 90 | function success(r) { 91 | res.end(JSON.stringify({ status: { success: true }, data: r })); 92 | } 93 | 94 | function failure(r) { 95 | res.end(JSON.stringify({ status: { success: false, reason: r } })); 96 | } 97 | 98 | path = path.replace(/^\/+/g, ""); // remove leading slashes. 99 | if(!path) return failure("Empty path."); 100 | 101 | params.sessionid = req.cookies["connect.sid"]; 102 | // TODO: Put additional authentication params 103 | 104 | 105 | req.on('data', function(chunk) { data += chunk; }); 106 | req.on('close', function() { res.end("Unexpected end of input.") }); 107 | req.on('end', function() { 108 | res.writeHead(200, {"Content-Type": "application/json"}); 109 | 110 | if(data) try { 111 | data = JSON.parse(data); 112 | } catch(e) { 113 | return failure('Invalid POSTdata: ' + e); 114 | } 115 | 116 | while(path) { 117 | path = path.replace(segpat, function(m, seg) { 118 | var segd = decodeURIComponent(seg); 119 | if(qo instanceof Table) { 120 | if(seg.indexOf(',') != -1) { 121 | seg = seg.split(',').map(decodeURIComponent); 122 | qo = qo.all({id: ["IN", seg]}); 123 | } 124 | else if("function" === typeof qo[segd]) { 125 | qo = qo[segd](data, params); 126 | } 127 | else { 128 | qo = qo.one({id: segd}); 129 | } 130 | } else { 131 | qo = qo[segd]; 132 | if(typeof qo == "function") qo = qo(data, params); 133 | } 134 | 135 | return ""; 136 | }); 137 | if(typeof qo !== "object") 138 | return failure("Invalid path before " + path); 139 | } 140 | 141 | switch(req.method) { 142 | case "GET": 143 | qo.then(success, failure); 144 | break; 145 | case "POST": 146 | if(qo instanceof Table) { 147 | qo.add(data).then(success, failure); 148 | } else if(qo instanceof Row) { 149 | qo.save(data).then(success, failure); 150 | } else { 151 | qo.then(success, failure); 152 | } 153 | break; 154 | default: 155 | res.end("That method isn't implemented yet"); 156 | } 157 | return null; // stop the editor from complaining. 158 | }); 159 | return null; 160 | } 161 | } -------------------------------------------------------------------------------- /lib/Query.js: -------------------------------------------------------------------------------- 1 | var defer = require("./defer.js"); 2 | 3 | module.exports = Query; 4 | 5 | function Query(database) { 6 | this.database = database; 7 | this.dialect = database.dialect; 8 | this.options = {}; 9 | }; 10 | 11 | Query.prototype.execute = function() { 12 | var data = defer(), self = this; 13 | this.database.query( 14 | this.sql(this, 0), 15 | function(err, value) { 16 | var tableMap = {}, i, out = {}; 17 | 18 | if(err) { 19 | data.resolve(defer.reject(err)); 20 | return; 21 | } 22 | 23 | function assemble(map, objs) { 24 | var i, obj = objs[map._ref]; 25 | 26 | for(i in map) { 27 | if(i == "_ref" || i == "_out") continue; 28 | obj[i] = map[i]._out? 29 | assemble(map[i], objs) : [assemble(map[i], objs)]; 30 | } 31 | 32 | return obj; 33 | } 34 | 35 | function merge(src, dst) { 36 | if(typeof src != "object" || typeof dst != "object") return dst; 37 | if(src instanceof Array && dst instanceof Array) { 38 | if(src[0].id != dst[0].id) src.push(dst[0]); 39 | } else for(i in dst) { 40 | src[i] = merge(src[i], dst[i]); 41 | } 42 | return src; 43 | } 44 | 45 | // If it's a select, I need to embed nested objects properly. 46 | 47 | if(self.verb == "select") { 48 | 49 | tableMap.A = self.database.tables[self.table]; 50 | 51 | for(i in self.options.joins) { 52 | tableMap[i] = self.database.tables[self.options.joins[i].table]; 53 | } 54 | 55 | //console.log("Table map is: ", tableMap); 56 | //console.log("Value is: ", value); 57 | 58 | for(i in value) { 59 | var j, row = value[i], tRef, colName, objs = {}, obj; 60 | 61 | for(j in tableMap) objs[j] = {}; 62 | 63 | for(j in row) { 64 | if(j == "parse" || j == "_typeCast") continue; 65 | tRef = j.substr(0,1); colName = j.substr(2); 66 | objs[tRef][colName] = row[j]; 67 | } 68 | 69 | obj = assemble(self.options.nestmap, objs); 70 | out[obj.id] = merge(out[obj.id], obj); 71 | } 72 | value = []; 73 | for(i in out) value.push(out[i]); 74 | } 75 | 76 | // If it's an insert or an update, I'm sending back the original 77 | // object. I should probably do a SELECT and send that, though. 78 | 79 | if(self.verb == "insert" && value.insertId > 0) { 80 | this.data.id = data.insertId; 81 | } 82 | if(self.verb == "insert" || self.verb == "update") { 83 | value = this.data; 84 | } 85 | 86 | data.resolve(value); 87 | } 88 | ); 89 | return data.promise; 90 | }; 91 | 92 | Query.prototype.sql = function (query, level) { 93 | var sql = "", i, delim = "", nd = this.dialect.nameDelim, self = this, join; 94 | 95 | if(level > 5) throw "Too deeply nested SQL"; 96 | 97 | if(typeof query !== "object") { 98 | return this.dialect.valueDelim + query + this.dialect.valueDelim; 99 | } 100 | 101 | if(query instanceof Date) { 102 | return this.dialect.valueDelim + query.toISOString() + this.dialect.valueDelim; 103 | } 104 | 105 | if(query instanceof Array) { 106 | return "(" + query. 107 | map(function(val) { return self.sql(val, level+1); }). 108 | join(",") + ")"; 109 | } 110 | 111 | function where(filters) { 112 | var sql = "", delim="", i; 113 | for(i in filters) { 114 | sql += delim + "A." + nd + i + nd; 115 | if(filters[i] instanceof Array) { 116 | sql += " " + filters[i][0] + " " + self.sql(filters[i][1], level+1); 117 | } else { 118 | sql += "=" + self.sql(filters[i], level+1); 119 | } 120 | 121 | delim = " AND "; 122 | } 123 | return sql; 124 | } 125 | 126 | function set(data) { 127 | var sql = "", delim=", ", i; 128 | for(i in data) 129 | sql += delim + nd + i + nd + "=" + self.sql(data[i], level+1); 130 | return sql.substring(2); 131 | } 132 | 133 | function values(data) { 134 | 135 | } 136 | 137 | function joinColumns(joins) { 138 | var i, sql = ""; 139 | for(i in joins) 140 | sql += ", " + tableColumns(i, joins[i].table); 141 | return sql; 142 | } 143 | 144 | function tableColumns(pre, columns) { 145 | if(typeof columns == "string") 146 | columns = Object.keys(self.database.tables[columns].columns); 147 | 148 | return columns.map(function(item) { 149 | return pre + "." + nd + item + nd + " AS " + nd + pre + "_" + item + nd 150 | }).join(", "); 151 | } 152 | 153 | if(query.verb == "select") { 154 | sql = "SELECT " + ( 155 | query.options.columns && query.options.columns.length? 156 | tableColumns("A", query.options.columns): 157 | tableColumns("A", query.table) + (query.options.joins? 158 | joinColumns(query.options.joins): "") 159 | ) + " FROM " +nd + query.table + nd + " AS A"; 160 | 161 | if(query.options.joins) { 162 | for(i in query.options.joins) { 163 | join = query.options.joins[i]; 164 | sql += " LEFT OUTER JOIN " + nd + join.table + nd + " AS " + i + 165 | " ON " + join.keyTable + "." + nd + join.key + nd + 166 | "=" + join.idTable + "." + nd + "id" + nd; 167 | } 168 | } 169 | 170 | if(query.filters) { 171 | sql += " WHERE " + where(query.filters); 172 | } 173 | } else if(query.verb == "insert") { 174 | sql = "INSERT INTO " + nd + query.table + nd + 175 | " SET " + set(query.data); 176 | } else if(query.verb == "update") { 177 | if(!query.filters) throw "Can't update without filters."; 178 | sql = "UPDATE " + nd + query.table + nd + 179 | " SET " + set(query.data) + 180 | " WHERE " + where(query.filters) + 181 | " LIMIT 1"; 182 | } else if(query.verb == "delete") { 183 | if(!query.filters) throw "Can't delete without filters."; 184 | sql = "DELETE FROM " + nd + query.table + nd + 185 | " WHERE " + where(query.filters); 186 | } 187 | 188 | if(!sql) { 189 | console.log(query.verb + " is not a valid verb."); 190 | } 191 | 192 | return level?"(" + sql + ")": sql; 193 | }; 194 | -------------------------------------------------------------------------------- /lib/Table.js: -------------------------------------------------------------------------------- 1 | var Row = require("./Row"), 2 | Column = require("./Column"), 3 | Relation = require("./Relation"), 4 | Query = require("./Query"), 5 | types = require('./types'); 6 | 7 | module.exports = Table; 8 | 9 | function Table(database, name, schema) { 10 | var i, t; 11 | 12 | if(arguments.length == 1) { 13 | t = database; 14 | database = t.database; 15 | name = t.name; 16 | schema = {}; 17 | for(i in t.schema) { 18 | schema[i] = t.schema[i].type; 19 | } 20 | } 21 | 22 | this.name = name; 23 | this.schema = {}; 24 | this.columns = {}; 25 | this.relations = {}; 26 | this.proto = {}; 27 | this.database = database; 28 | 29 | if(schema) for(i in schema) { 30 | if(schema[i].name == "relation") { 31 | if(schema[i].options.outbound) 32 | schema[i].options.table = schema[schema[i].options.column].table; 33 | this.relation(i, schema[i].options); 34 | } else { 35 | this.column(i, schema[i]); 36 | } 37 | } 38 | 39 | this.query = new Query(database); 40 | this.query.table = name; 41 | 42 | this.finalize(); 43 | }; 44 | 45 | function attachOptions(query, table, options) { 46 | var i, joins = {}, refNo = 1; 47 | 48 | function join(nestMap, pTab, pRef) { 49 | var i, res = {}, ref, rel, t; 50 | 51 | for(i in nestMap) { 52 | if(i == "_ref" || i == "_out") continue; 53 | ref = String.fromCharCode(refNo+65); refNo++; 54 | rel = pTab.relations[i]; 55 | // console.log(i, pTab.relations); 56 | joins[ref] = { 57 | table: rel.targetTable, 58 | key: rel.targetColumn, 59 | keyTable: rel.outbound? pRef: ref, 60 | idTable: rel.outbound? ref: pRef 61 | }; 62 | 63 | if(rel.targetRelation) { 64 | t = {}; t[rel.targetRelation] = nestMap[i]; 65 | join(t, table.database.tables[rel.targetTable], ref); 66 | if(typeof(nestMap[i]) !== "object") nestMap[i] = {}; 67 | nestMap[i]._ref = ref; 68 | nestMap[i]._out = rel.outbound && 69 | table.database.tables[rel.targetTable].relations[ 70 | rel.targetRelation].outbound; 71 | } 72 | else if(typeof nestMap[i] == "object") { 73 | join(nestMap[i], table.database.tables[rel.targetTable], ref); 74 | nestMap[i]["_out"] = rel.outbound; 75 | } else { 76 | nestMap[i] = { _ref: ref, _out: rel.outbound }; 77 | } 78 | } 79 | nestMap["_ref"] = pRef; 80 | return ref; 81 | } 82 | 83 | if(options.embed) { 84 | join(options.embed, table, "A"); 85 | query.options.joins = joins; 86 | query.options.nestmap = options.embed; 87 | } 88 | } 89 | 90 | Table.prototype.one = function (filters, options) { 91 | var ret = new Row(this); 92 | ret.query.verb = "select"; 93 | ret.query.filters = filters || {}; 94 | if(options) attachOptions(ret.query, this, options); 95 | return ret; 96 | }; 97 | 98 | Table.prototype.all = function (filters, options) { 99 | var ret = new Table(this); 100 | ret.query.verb = "select"; 101 | ret.query.filters = filters || {}; 102 | if(options) attachOptions(ret.query, this, options); 103 | return ret; 104 | }; 105 | 106 | Table.prototype.related = function(relName, filters, options) { 107 | var sub = Object.create(this.query), res, relation = this.relations[relName]; 108 | res = new Table(this.database.tables[relation.targetTable]); 109 | sub.verb = res.query.verb = "select"; 110 | 111 | if(relation.outbound) { 112 | sub.options.columns = [relation.targetColumn]; 113 | res.query.filters = filters || {}; 114 | res.query.filters.id = ["IN", sub]; 115 | } else { 116 | sub.options.columns = ["id"]; 117 | res.query.filters = filters || {}; 118 | res.query.filters[field.targetColumn] = ["IN", sub]; 119 | } 120 | return res; 121 | } 122 | 123 | Table.prototype.count = function (filters) { /* not implemented */ }; 124 | Table.prototype.sum = function(column, filters) { /* not implemented */ }; 125 | 126 | Table.prototype.add = function (data) { 127 | var ret = new Table(this); 128 | ret.query.verb = "insert"; 129 | ret.query.data = data; 130 | 131 | return ret.query.execute(); 132 | } 133 | 134 | Table.prototype.del = function (filters) { 135 | var ret = new Table(this); 136 | ret.query.verb = "delete"; 137 | ret.query.filters = filters; 138 | 139 | return ret.query.execute(); 140 | } 141 | 142 | Table.prototype.column = function(name, type) { 143 | var relname = ""; 144 | 145 | if(typeof type === "function") type = type(); // optional parameterization 146 | 147 | if(type && type.name === "foreign") { 148 | if(name.substr(-2) === "Id") { 149 | relname = name.substr(0, name.length - 2); 150 | } else if(name.substr(-3) === "_id") { 151 | relname = name.substr(0, name.length - 3); 152 | } 153 | 154 | if(relname) this.relation(relname, { 155 | table: type.table, column: name, outbound: true 156 | }); 157 | } 158 | 159 | return this.schema[name] = this.columns[name] = 160 | new Column(this, name, type); 161 | } 162 | 163 | Table.prototype.relation = function(name, options) { 164 | return this.schema[name] = this.relations[name] = 165 | new Relation(this, name, options); 166 | } 167 | 168 | Table.prototype.then = function(callback, errback) { 169 | // TODO: on successful execution, extend the returned value with the 170 | // appropriate prototype, attach a "save" method etc. 171 | this.query.verb = "select"; 172 | return this.query.execute().then(callback, errback); 173 | } 174 | 175 | Table.prototype.createSql = function(force) { 176 | var i, delim="", type, sql = "CREATE TABLE ", 177 | nd = this.database.dialect.nameDelim; 178 | 179 | this.finalize(); 180 | 181 | sql += (force? "": "IF NOT EXISTS ") + nd + this.name + nd + " ("; 182 | 183 | for(i in this.columns) { 184 | sql += delim + this.columns[i].createSql(); 185 | delim = ", " 186 | } 187 | 188 | sql += ")"; 189 | 190 | return sql; 191 | } 192 | 193 | Table.prototype.finalize = function() { 194 | if(!this.schema.id) 195 | this.schema.id = this.columns.id = this.column("id", types.Serial); 196 | 197 | this.schema.id.index = "primary"; 198 | 199 | this.column = null; 200 | this.relation = null; 201 | } 202 | 203 | --------------------------------------------------------------------------------