├── .gitignore ├── .meteor ├── .gitignore └── packages ├── Docs.md ├── History.md ├── LICENSE.txt ├── README.md ├── client ├── example │ ├── employee.html │ ├── employee.js │ ├── main.html │ ├── schema.html │ ├── schema.js │ ├── select.html │ ├── select.js │ ├── view.html │ └── view.js └── lib │ ├── autils.js │ └── sql │ ├── select.js │ └── table.js ├── common └── lib │ ├── devwik.js │ └── vendor │ └── squel.js ├── sampledatabase.sql └── server ├── client.js ├── dbconfig.js ├── dbinit.js ├── dblib.js ├── lib └── lib.js ├── poll.js ├── select.js ├── table.js ├── tests.js └── view.js /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dbdefPassAgain.js 3 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | insecure 7 | preserve-inputs 8 | bootstrap 9 | underscore 10 | -------------------------------------------------------------------------------- /Docs.md: -------------------------------------------------------------------------------- 1 | ## Full server side DB access 2 | 3 | You have full access to all SQL statements on the server: Select, inserts, updates, create table, etc. 4 | 5 | ## Selects 6 | Run Devwik.SQL.execStatement. You iterate over the array it returns. 7 | ``` 8 | var rows = Devwik.SQL.execStatement('select * from employees'); 9 | _.each(rows, function(row){ //For each table in the db 10 | //now use row.firstName, row.lastName ... 11 | }); 12 | ``` 13 | 14 | ## Other statements 15 | 16 | Same as selects except no data is returned. 17 | ``` 18 | Devwik.SQL.execStatement('update employees set officeCode = 7 where officeCode = 6'); 19 | ``` 20 | 21 | ## Using squel 22 | Squel, http://hiddentao.github.com/squel, is supported making construction of query strings less error prone than having to concatenate them. 23 | ``` 24 | squel.select().from('employees').where(key + " = '" + id + "'"); 25 | ``` 26 | 27 | ## Escaping user input 28 | 29 | Use Devwik.SQL.escape() to escape user input to protect from SQL injections. 30 | The following code demonstrates combining the use of squel and escaping. 31 | ``` 32 | var statement = squel.insert().into(tableName); 33 | _.each(args, function(value, key) { 34 | value = Devwik.SQL.escape(value); 35 | statement.set(key, value); 36 | }); 37 | ``` 38 | 39 | 40 | ## Tables 41 | *Tables need to have a unique id* so that the driver can identify rows that are sent to the client. Tables without a unique id are ignored. 42 | 43 | On startup the driver automatically finds out the tables you have in the db and creates a Table object for each one, and publishes them to the client. 44 | 45 | ## Client Side 46 | Subscribe to a table's data by declaring it client side. 47 | ``` 48 | var Employee = new Meteor.Table('employees'); 49 | ``` 50 | ### Viewing data: find() 51 | Use the standard Meteor client side Mongo API, http://docs.meteor.com/#find, to fetch data or use it in templates. 52 | 53 | ## Insert 54 | 55 | Insert takes two arguments: an object with the data to insert and a callback function that gets passed and err and value params. Value contains the row id of the inserted row if any. 56 | ``` 57 | Employee.insert(insert, function(err, value) { 58 | ... 59 | }); 60 | ``` 61 | ## Update 62 | Insert takes three arguments: an object with the data to insert, the id of the row to update, and a callback function that gets passed and err and value params. 63 | ``` 64 | update = {}; 65 | update.firstName = $('#updateFirst').val(); 66 | update.lastName = $('#updateLast').val(); 67 | update.email = $('#updateEmail').val(); 68 | update.jobTitle = $('#updateTitle').val(); 69 | Employee.update(update, id, function(err, value) { 70 | ... 71 | } 72 | ``` 73 | 74 | ## delete 75 | Delete takes two arguments: id of the row to update, and a callback function that gets passed and err and value params. 76 | ``` 77 | Employee.remove(id, function(err, value) { 78 | ... 79 | } 80 | ``` 81 | 82 | # Views 83 | 84 | ## Limitations 85 | Currently you can only use simple views that include the keys from the original tables. 86 | So 87 | ``` 88 | create view bar as select firstName, lastName, email, jobTitle, employees.officeCode, city, addressLine1, state, country from offices, employees 89 | where employees.officeCode = offices.officeCode limit 3; 90 | ``` 91 | works fine. It gives us employee and office info for each employee and includes keys in each table. 92 | On the other hand the following view: 93 | ``` 94 | create view empOffice as select count(*) empNumber, offices.* from employees, offices where offices.officeCode = employees.officecode group by officeCode; 95 | ``` 96 | Aggregates the number of employees in the first column. When a new employee record is inserted, there's no obvious way to tell which rows in this view changed. At this point, this kind of view is not supported. 97 | *Updatable views are not supported.* You can't insert, update or delete from a view. 98 | 99 | ## Usage 100 | Once you create a view in the DB, subject to the above limitations, you use it the same as you would a table. The driver creates the objects server side, and you create a Table object using the view name. 101 | 102 | 103 | # Selects 104 | 105 | Unlike views, there are not limitations to what's included in a select statement. Selects, however *are not reactive*. This means that changes to rows shown in a resultset from a select will not change until the user reloads the page. 106 | 107 | ## Server 108 | You create selects on the server using the following syntax. The first argument is the name of the select, and the second is the statement. 109 | ``` 110 | Devwik.SQL.Select('empsCities', 'select employees.*, offices.city from employees, offices where offices.officeCode = employees.officecode'); 111 | ``` 112 | 113 | ## Client 114 | Just define which select you're using 115 | ``` 116 | var Select = new Meteor.Select('empsCities'); 117 | Select.find({}, {sort: {employeeNumber: -1}}); 118 | ``` 119 | 120 | # Transactions 121 | 122 | ## Engine 123 | You need to use an engine that supports transaction such as *innodb*, otherwise all transaction related statements are ignored. 124 | 125 | ## Usage 126 | 127 | ### Automatic COMMIT OR ROLLBACK 128 | The following code demonstrates how to put multiple statements in a transaction. 129 | 1. You create a transaction object using new Devwik.SQL.Transaction. 130 | 2. You pass the transaction object to each execStatement you call. 131 | 3. You call end() on the object when you're done with the transaction. 132 | ``` 133 | var transaction = new Devwik.SQL.Transaction(); 134 | if(transaction) { 135 | //employeeNumber is a unique key at least one of these should fail 136 | Devwik.SQL.execStatement("INSERT INTO employees (employeeNumber,firstName, lastName, email, jobTitle) VALUES (1759, 'aaaa', 'bbb', 'ddd', 'ccc')", transaction); 137 | Devwik.SQL.execStatement("INSERT INTO employees (employeeNumber,firstName, lastName, email, jobTitle) VALUES (1759, 'aaaa', 'bbb', 'ddd', 'ccc')", transaction); 138 | transaction.end(); 139 | } 140 | ``` 141 | If any of the statements fail, the rest the transaction is rolled back. Otherwise, the transaction is committed. 142 | 143 | 144 | ### Manual COMMIT OR ROLLBACK 145 | You can create and manage transactions manually 146 | ``` 147 | var transaction = new Devwik.SQL.Transaction();//Create the transaction 148 | if(transaction) { 149 | ... 150 | if(/* good stuff */) { 151 | transaction.commit(); 152 | } else { 153 | transaction.rollback(); 154 | } 155 | } 156 | ``` 157 | 158 | *Do not use Exception handling to catch SQL errors.* node.js exceptions don't work correctly. 159 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # v0.21 2 | * Make it work with Meteor 0.6+ new package system. 3 | 4 | # v0.2 5 | * Support for reactive joins through views. Any changes in the underlying tables automatically shows up in the view. 6 | * Migrated to use squel() syntax where appropriate. Cleaner. 7 | * Use Devwik.SQL.escape() to sanitize data on user input. 8 | * Added API documentation. 9 | 10 | # v0.1 11 | * Full server side support of select, insert, update and delete on a table 12 | * All changes get propagated to all subscribed clients as with MongoDb 13 | * Changes to the db from other apps are detected immediately (100ms, configurable), and propagated to the client 14 | * Light weight implementation 15 | * Changes are handled by triggers, no diffs to existing queries needed 16 | * Polling is done on a single indexed table, very little overhead. 17 | * includes https://github.com/hiddentao/squel for cleaner query construction 18 | * Partial support for general select statements. They work correctly, but are not reactive 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Dror Matalon 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Meteor SQL 2 | ========== 3 | 4 | # Not compatible with the latest versions of Meteor 5 | 6 | This is an initial implementation of Meteor SQL. It currently only supports MySQL. 7 | 8 | # Features 9 | * Full server side support of select, insert, update and delete on a table 10 | * All changes get propagated to all subscribed clients as with MongoDb 11 | * Changes to the db from other apps are detected immediately (100ms, configurable), and propagated to the client 12 | * Support for reactive joins through views. Any changes in the underlying tables automatically shows up in the view. 13 | * Light weight implementation 14 | * Changes are handled by triggers, no diffs to existing queries needed 15 | * Polling is done on a single indexed table, very little overhead. 16 | * includes https://github.com/hiddentao/squel for cleaner query construction 17 | * Partial support for general select statements. They work correctly, but are not reactive 18 | 19 | # Limitations 20 | * Client side the collection still use mongo syntax for find() 21 | * All tables need to have a unique id 22 | * Insert, Update and Delete operations on the client don't update the data locally. Instead they run on the server and then the server refreshes the client's data. This could result in slower refresh times, but guarantees that the client always sees data that has been comited to the db. It also means that unlike minmongo, the full range of SQL options are available to the client. 23 | 24 | # Installation 25 | 26 | * Standard mysql set up 27 | * Install mysql 28 | * create database meteor; 29 | * grant all on meteor.\* to meteor@'localhost' IDENTIFIED BY 'xxxxx2344958889d'; #Change the password to something else 30 | * Now install the mysql client for node.js 31 | * run meteor in the app's directory so that it builds the hierarchy in the .meteor directory 32 | * cd .meteor/local/build/server/ 33 | * npm install mysql 34 | * Change the database config params in server/dbconfig.js to match the password you entered above as well as anything else needed 35 | 36 | # Implementation Approach 37 | * insert into the audit trail table information about insert, update, delete 38 | * poll the audit table 39 | * When there is a change, publish it using Meteor's standard Meteor.publish 40 | * Client operations, insert, update, delete use Meteor.call 41 | 42 | # Future 43 | * Make select statement reactive 44 | * Support prepared statements 45 | * Support any kind of views 46 | * Provide a way to automatically generate forms 47 | -------------------------------------------------------------------------------- /client/example/employee.html: -------------------------------------------------------------------------------- 1 | 44 | 45 | 71 | 72 | 73 | 98 | 99 | 100 | 103 | -------------------------------------------------------------------------------- /client/example/employee.js: -------------------------------------------------------------------------------- 1 | var notDone = true; 2 | var Employee = new Meteor.Table('employees'); 3 | Meteor.subscribe('meteor_tables'); 4 | var Tables = new Meteor.Collection('meteor_tables'); 5 | 6 | Template.devwikEmployees.rendered = function () { 7 | if (notDone) {//do it once 8 | notDone = false; 9 | Devwik.Utils.clickButton('#insert', function(event) { 10 | var insert = {}; 11 | insert.firstName = $('#inputFirst').val(); 12 | insert.lastName = $('#inputLast').val(); 13 | insert.email = $('#inputEmail').val(); 14 | insert.jobTitle = $('#inputTitle').val(); 15 | console.log(insert); 16 | Employee.insert(insert, function(err, value) { 17 | if (err) { 18 | alert(_.values(err)); 19 | } else { 20 | $('#insertResult').html('Inserted:' + value); 21 | console.log(value); 22 | } 23 | }); 24 | }); 25 | 26 | Devwik.Utils.clickButton('#update', function(event) { 27 | var id= $('#updateId').val(); 28 | update = {}; 29 | update.firstName = $('#updateFirst').val(); 30 | update.lastName = $('#updateLast').val(); 31 | update.email = $('#updateEmail').val(); 32 | update.jobTitle = $('#updateTitle').val(); 33 | Employee.update(update, id, function(err, value) { 34 | if (err) { 35 | alert(_.values(err)); 36 | } else { 37 | console.log(value); 38 | } 39 | }); 40 | return(false); 41 | }); 42 | 43 | Devwik.Utils.clickButton('#remove', function(event) { 44 | var remove= $('#fieldRemove').val(); 45 | console.log(remove); 46 | Employee.remove(remove, function(err, value) { 47 | if (err) { 48 | alert(_.values(err)); 49 | } else { 50 | $('#deleteResult').html('Deleted:' + value.affectedRows); 51 | console.log(value); 52 | } 53 | }); 54 | }); 55 | 56 | } 57 | }; 58 | 59 | Template.employeeRows.employees = function () { 60 | return Employee.find({}, {sort: {employeeNumber: -1}}); 61 | }; 62 | 63 | Template.devwikEmployees.eSelects = function () { 64 | return Employee.find({}, {sort: {employeeNumber: -1}}); 65 | }; 66 | 67 | 68 | function deleteEmployee(number) { 69 | console.log('remove:' + number); 70 | Employee.remove(number, function(err, value) { 71 | if (err) { 72 | alert(_.values(err)); 73 | } 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /client/example/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Meteor SQL Demo 4 | 5 | 11 | 12 | 13 | 14 | 15 |
16 |

Meteor SQL

17 | 23 | 24 |
25 |
26 |
27 | {{> devwikEmployees}} 28 |
29 |
30 |
31 | {{> devwikView}} 32 |
33 |
34 | {{> devwikSelects}} 35 |
36 |
37 | {{> devwikTables}} 38 |
39 |
40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/example/schema.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 46 | 47 | 63 | 64 | -------------------------------------------------------------------------------- /client/example/schema.js: -------------------------------------------------------------------------------- 1 | //This is not a real table. Just information about the database 2 | //schema 3 | Template.devwikTables.rendered = function () { 4 | }; 5 | 6 | Template.devwikTables.tables = function () { 7 | return Tables.find({}); 8 | }; 9 | -------------------------------------------------------------------------------- /client/example/select.html: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /client/example/select.js: -------------------------------------------------------------------------------- 1 | var empCities = new Meteor.Select('empsPerCity'); 2 | console.log(empCities); 3 | 4 | 5 | Template.devwikSelects.selects = function () { 6 | console.log('selects'); 7 | return empCities.find({}, {sort: {employeeNumber: -1}}); 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /client/example/view.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /client/example/view.js: -------------------------------------------------------------------------------- 1 | var empCity = new Meteor.Table('empCity'); 2 | 3 | 4 | Template.devwikView.viewRows = function () { 5 | return empCity.find({}, {sort: {employeeNumber: -1}}); 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /client/lib/autils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Devwik.Utils: Various utility functions 4 | */ 5 | var Devwik = function() {}; 6 | Devwik.Utils = function() {}; 7 | 8 | /** 9 | * Devwik.Utils.clickButton: 10 | * Typical use: 11 | * 12 | * @param {HTML} selection 13 | * @param {Function} Action on click 14 | */ 15 | 16 | Devwik.Utils.clickButton = function(selector, func) { 17 | $(selector).on("click", function(event){ 18 | func(event); 19 | return(false);//prevent default button behavior 20 | }); 21 | }; 22 | 23 | /** 24 | * Devwik.Utils.callback: 25 | * A generic callback to a Meteor action that is used to let the user know when there's an error 26 | * 27 | * @param {Object} error 28 | */ 29 | 30 | Devwik.Utils.callback = function(error) { 31 | if (error) { 32 | alert(_.values(error)); 33 | console.log(_.values(error)); 34 | } 35 | }; 36 | 37 | Devwik.Utils.getURLParameter = function(name) { 38 | return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null; 39 | }; 40 | 41 | 42 | /** 43 | * Devwik Message: a message that displays around the toolbar to let the user know something happened 44 | * Similar to message in gmail when you archive a message 45 | * 46 | * @param {String} message: the message you want to show 47 | * @param {int} time: the time in milliseconds before hiding the message 48 | */ 49 | 50 | Devwik.Utils.message = function(message, time) { 51 | 52 | $('#currentMessage').remove();//remove previous one if any 53 | var element = $(''); 54 | element.append(message); 55 | $('body').append(element); 56 | element.show('fast'); 57 | if(time) { //if we've got a time, remove after that time 58 | Meteor.setTimeout(function(){ 59 | $(element).fadeOut("slow", function () { 60 | $(element).remove(); 61 | }); 62 | }, time); 63 | } 64 | }; 65 | 66 | /** 67 | * Devwik Dialog: display a message in a centered dialog 68 | * 69 | * @param {String} message: the message you want to show 70 | * @param {boolean} animate: animate the text? 71 | */ 72 | Devwik.Utils.dialog = function(message, animate) { 73 | var divId = Meteor.uuid(); 74 | var div = $('
' + message + '
'); 75 | console.log('dialog'); 76 | dialog = div.dialog({ 77 | width: 400, 78 | height: 300 79 | });//show the dialog 80 | //Animate the message 81 | var element = $('#' + divId); 82 | 83 | function animateMessage() { 84 | element.show("slow").animate({"fontSize":"30px"},2000).animate({"fontSize":"50px"},2000); 85 | Meteor.setTimeout(animateMessage, 200); 86 | } 87 | if (animate) { 88 | 89 | animateMessage(); 90 | return dialog; 91 | } 92 | }; 93 | 94 | /** 95 | * Devwik Link: create a clickable element 96 | * 97 | * @param {String} text: the text you want to show 98 | * @param {function} func: the function to call on click 99 | */ 100 | Devwik.Utils.link = function(text, func) { 101 | var element = '' + text + ' '; 102 | $('body').on('click', $(element), func); 103 | return(element); 104 | }; 105 | 106 | 107 | /* 108 | * http://stackoverflow.com/questions/868889/submit-jquery-ui-dialog-on-enter 109 | * By default Enter submits the form 110 | */ 111 | /* need jquery-ui for this 112 | $(function() { 113 | $.extend($.ui.dialog.prototype.options, { 114 | open: function() { 115 | var $this = $(this); 116 | 117 | // focus first button and bind enter to it 118 | //$this.parent().find('.ui-dialog-buttonpane button:first').focus(); 119 | //When Enter is hit, submit the form with the first button 120 | $this.keypress(function(e) { 121 | if( e.keyCode == $.ui.keyCode.ENTER ) { 122 | $this.parent().find('.ui-dialog-buttonpane button:first').click(); 123 | return false; 124 | } 125 | }); 126 | } 127 | }); 128 | }); 129 | */ 130 | -------------------------------------------------------------------------------- /client/lib/sql/select.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Meteor.Select = function(name) { 3 | Meteor.subscribe(name); 4 | var myCollection = new Meteor.Collection(name); 5 | 6 | return(myCollection); 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /client/lib/sql/table.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Meteor.Table = function(name) { 3 | Meteor.subscribe(name); 4 | var myCollection = new Meteor.Collection(name); 5 | 6 | myCollection.insert = function (args, callback) { 7 | Meteor.call('SQLinsert', this._name, args, callback); 8 | }; 9 | 10 | myCollection.update = function (args, id, callback) { 11 | try { 12 | Meteor.call('SQLupdate', this._name, args, id, callback); 13 | console.log('after update'); 14 | } catch (err) { 15 | console.log(err); 16 | } 17 | }; 18 | 19 | myCollection.remove = function (id, callback) { 20 | try { 21 | Meteor.call('SQLremove', this._name, id, callback); 22 | } catch (err) { 23 | console.log(err); 24 | } 25 | }; 26 | return(myCollection); 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /common/lib/devwik.js: -------------------------------------------------------------------------------- 1 | if (typeof Devwik == 'undefined') { 2 | Devwik = function() {}; 3 | } 4 | 5 | //based on http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ 6 | Devwik.toType = function(obj) { 7 | return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); 8 | }; 9 | -------------------------------------------------------------------------------- /common/lib/vendor/squel.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012 Ramesh Nair (hiddentao.com) 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | 27 | (function() { 28 | var Cloneable, DefaultQueryBuilderOptions, Delete, Expression, Insert, JoinWhereOrderLimit, QueryBuilder, Select, Update, WhereOrderLimit, _export, _extend, 29 | __slice = [].slice, 30 | __hasProp = {}.hasOwnProperty, 31 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 32 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 33 | 34 | _extend = function() { 35 | var dst, k, sources, src, v, _i, _len; 36 | dst = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 37 | if (sources) { 38 | for (_i = 0, _len = sources.length; _i < _len; _i++) { 39 | src = sources[_i]; 40 | if (src) { 41 | for (k in src) { 42 | if (!__hasProp.call(src, k)) continue; 43 | v = src[k]; 44 | dst[k] = v; 45 | } 46 | } 47 | } 48 | } 49 | return dst; 50 | }; 51 | 52 | Cloneable = (function() { 53 | 54 | function Cloneable() {} 55 | 56 | Cloneable.prototype.clone = function() { 57 | var newInstance; 58 | newInstance = new this.constructor; 59 | return _extend(newInstance, JSON.parse(JSON.stringify(this))); 60 | }; 61 | 62 | return Cloneable; 63 | 64 | })(); 65 | 66 | Expression = (function() { 67 | var _toString; 68 | 69 | Expression.prototype.tree = null; 70 | 71 | Expression.prototype.current = null; 72 | 73 | function Expression() { 74 | this.toString = __bind(this.toString, this); 75 | 76 | this.or = __bind(this.or, this); 77 | 78 | this.and = __bind(this.and, this); 79 | 80 | this.end = __bind(this.end, this); 81 | 82 | this.or_begin = __bind(this.or_begin, this); 83 | 84 | this.and_begin = __bind(this.and_begin, this); 85 | 86 | var _this = this; 87 | this.tree = { 88 | parent: null, 89 | nodes: [] 90 | }; 91 | this.current = this.tree; 92 | this._begin = function(op) { 93 | var new_tree; 94 | new_tree = { 95 | type: op, 96 | parent: _this.current, 97 | nodes: [] 98 | }; 99 | _this.current.nodes.push(new_tree); 100 | _this.current = _this.current.nodes[_this.current.nodes.length - 1]; 101 | return _this; 102 | }; 103 | } 104 | 105 | Expression.prototype.and_begin = function() { 106 | return this._begin('AND'); 107 | }; 108 | 109 | Expression.prototype.or_begin = function() { 110 | return this._begin('OR'); 111 | }; 112 | 113 | Expression.prototype.end = function() { 114 | if (!this.current.parent) { 115 | throw new Error("begin() needs to be called"); 116 | } 117 | this.current = this.current.parent; 118 | return this; 119 | }; 120 | 121 | Expression.prototype.and = function(expr) { 122 | if (!expr || "string" !== typeof expr) { 123 | throw new Error("expr must be a string"); 124 | } 125 | this.current.nodes.push({ 126 | type: 'AND', 127 | expr: expr 128 | }); 129 | return this; 130 | }; 131 | 132 | Expression.prototype.or = function(expr) { 133 | if (!expr || "string" !== typeof expr) { 134 | throw new Error("expr must be a string"); 135 | } 136 | this.current.nodes.push({ 137 | type: 'OR', 138 | expr: expr 139 | }); 140 | return this; 141 | }; 142 | 143 | Expression.prototype.toString = function() { 144 | if (null !== this.current.parent) { 145 | throw new Error("end() needs to be called"); 146 | } 147 | return _toString(this.tree); 148 | }; 149 | 150 | _toString = function(node) { 151 | var child, nodeStr, str, _i, _len, _ref; 152 | str = ""; 153 | _ref = node.nodes; 154 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 155 | child = _ref[_i]; 156 | if (child.expr != null) { 157 | nodeStr = child.expr; 158 | } else { 159 | nodeStr = _toString(child); 160 | if ("" !== nodeStr) { 161 | nodeStr = "(" + nodeStr + ")"; 162 | } 163 | } 164 | if ("" !== nodeStr) { 165 | if ("" !== str) { 166 | str += " " + child.type + " "; 167 | } 168 | str += nodeStr; 169 | } 170 | } 171 | return str; 172 | }; 173 | 174 | return Expression; 175 | 176 | })(); 177 | 178 | DefaultQueryBuilderOptions = { 179 | autoQuoteTableNames: false, 180 | autoQuoteFieldNames: false, 181 | nameQuoteCharacter: '`', 182 | usingValuePlaceholders: false 183 | }; 184 | 185 | QueryBuilder = (function(_super) { 186 | 187 | __extends(QueryBuilder, _super); 188 | 189 | function QueryBuilder(options) { 190 | this.options = _extend({}, DefaultQueryBuilderOptions, options); 191 | } 192 | 193 | QueryBuilder.prototype._getObjectClassName = function(obj) { 194 | var arr; 195 | if (obj && obj.constructor && obj.constructor.toString) { 196 | arr = obj.constructor.toString().match(/function\s*(\w+)/); 197 | if (arr && arr.length === 2) { 198 | return arr[1]; 199 | } 200 | } 201 | return void 0; 202 | }; 203 | 204 | QueryBuilder.prototype._sanitizeCondition = function(condition) { 205 | var c, t; 206 | t = typeof condition; 207 | c = this._getObjectClassName(condition); 208 | if ('Expression' !== c && "string" !== t) { 209 | throw new Error("condition must be a string or Expression instance"); 210 | } 211 | if ('Expression' === t || 'Expression' === c) { 212 | condition = condition.toString(); 213 | } 214 | return condition; 215 | }; 216 | 217 | QueryBuilder.prototype._sanitizeName = function(value, type) { 218 | if ("string" !== typeof value) { 219 | throw new Error("" + type + " must be a string"); 220 | } 221 | return value; 222 | }; 223 | 224 | QueryBuilder.prototype._sanitizeField = function(item) { 225 | var sanitized; 226 | sanitized = this._sanitizeName(item, "field name"); 227 | if (this.options.autoQuoteFieldNames) { 228 | return "" + this.options.nameQuoteCharacter + sanitized + this.options.nameQuoteCharacter; 229 | } else { 230 | return sanitized; 231 | } 232 | }; 233 | 234 | QueryBuilder.prototype._sanitizeTable = function(item) { 235 | var sanitized; 236 | sanitized = this._sanitizeName(item, "table name"); 237 | if (this.options.autoQuoteTableNames) { 238 | return "" + this.options.nameQuoteCharacter + sanitized + this.options.nameQuoteCharacter; 239 | } else { 240 | return sanitized; 241 | } 242 | }; 243 | 244 | QueryBuilder.prototype._sanitizeAlias = function(item) { 245 | return this._sanitizeName(item, "alias"); 246 | }; 247 | 248 | QueryBuilder.prototype._sanitizeLimitOffset = function(value) { 249 | value = parseInt(value); 250 | if (0 > value || isNaN(value)) { 251 | throw new Error("limit/offset must be >=0"); 252 | } 253 | return value; 254 | }; 255 | 256 | QueryBuilder.prototype._sanitizeValue = function(item) { 257 | var t; 258 | t = typeof item; 259 | if (null !== item && "string" !== t && "number" !== t && "boolean" !== t) { 260 | throw new Error("field value must be a string, number, boolean or null"); 261 | } 262 | return item; 263 | }; 264 | 265 | QueryBuilder.prototype._formatValue = function(value) { 266 | if (null === value) { 267 | value = "NULL"; 268 | } else if ("boolean" === typeof value) { 269 | value = value ? "TRUE" : "FALSE"; 270 | } else if ("number" !== typeof value) { 271 | if (false === this.options.usingValuePlaceholders) { 272 | value = "'" + value + "'"; 273 | } 274 | } 275 | return value; 276 | }; 277 | 278 | return QueryBuilder; 279 | 280 | })(Cloneable); 281 | 282 | WhereOrderLimit = (function(_super) { 283 | 284 | __extends(WhereOrderLimit, _super); 285 | 286 | function WhereOrderLimit(options) { 287 | this._limitString = __bind(this._limitString, this); 288 | 289 | this._orderString = __bind(this._orderString, this); 290 | 291 | this._whereString = __bind(this._whereString, this); 292 | 293 | this.limit = __bind(this.limit, this); 294 | 295 | this.order = __bind(this.order, this); 296 | 297 | this.where = __bind(this.where, this); 298 | WhereOrderLimit.__super__.constructor.call(this, options); 299 | this.wheres = []; 300 | this.orders = []; 301 | this.limits = null; 302 | } 303 | 304 | WhereOrderLimit.prototype.where = function(condition) { 305 | condition = this._sanitizeCondition(condition); 306 | if ("" !== condition) { 307 | this.wheres.push(condition); 308 | } 309 | return this; 310 | }; 311 | 312 | WhereOrderLimit.prototype.order = function(field, asc) { 313 | if (asc == null) { 314 | asc = true; 315 | } 316 | field = this._sanitizeField(field); 317 | this.orders.push({ 318 | field: field, 319 | dir: asc ? "ASC" : "DESC" 320 | }); 321 | return this; 322 | }; 323 | 324 | WhereOrderLimit.prototype.limit = function(max) { 325 | max = this._sanitizeLimitOffset(max); 326 | this.limits = max; 327 | return this; 328 | }; 329 | 330 | WhereOrderLimit.prototype._whereString = function() { 331 | if (0 < this.wheres.length) { 332 | return " WHERE (" + this.wheres.join(") AND (") + ")"; 333 | } else { 334 | return ""; 335 | } 336 | }; 337 | 338 | WhereOrderLimit.prototype._orderString = function() { 339 | var o, orders, _i, _len, _ref; 340 | if (0 < this.orders.length) { 341 | orders = ""; 342 | _ref = this.orders; 343 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 344 | o = _ref[_i]; 345 | if ("" !== orders) { 346 | orders += ", "; 347 | } 348 | orders += "" + o.field + " " + o.dir; 349 | } 350 | return " ORDER BY " + orders; 351 | } else { 352 | return ""; 353 | } 354 | }; 355 | 356 | WhereOrderLimit.prototype._limitString = function() { 357 | if (this.limits) { 358 | return " LIMIT " + this.limits; 359 | } else { 360 | return ""; 361 | } 362 | }; 363 | 364 | return WhereOrderLimit; 365 | 366 | })(QueryBuilder); 367 | 368 | JoinWhereOrderLimit = (function(_super) { 369 | 370 | __extends(JoinWhereOrderLimit, _super); 371 | 372 | function JoinWhereOrderLimit(options) { 373 | this._joinString = __bind(this._joinString, this); 374 | 375 | this.outer_join = __bind(this.outer_join, this); 376 | 377 | this.right_join = __bind(this.right_join, this); 378 | 379 | this.left_join = __bind(this.left_join, this); 380 | 381 | this.join = __bind(this.join, this); 382 | JoinWhereOrderLimit.__super__.constructor.call(this, options); 383 | this.joins = []; 384 | } 385 | 386 | JoinWhereOrderLimit.prototype.join = function(table, alias, condition, type) { 387 | if (type == null) { 388 | type = 'INNER'; 389 | } 390 | table = this._sanitizeTable(table); 391 | if (alias) { 392 | alias = this._sanitizeAlias(alias); 393 | } 394 | if (condition) { 395 | condition = this._sanitizeCondition(condition); 396 | } 397 | this.joins.push({ 398 | type: type, 399 | table: table, 400 | alias: alias, 401 | condition: condition 402 | }); 403 | return this; 404 | }; 405 | 406 | JoinWhereOrderLimit.prototype.left_join = function(table, alias, condition) { 407 | if (alias == null) { 408 | alias = null; 409 | } 410 | if (condition == null) { 411 | condition = null; 412 | } 413 | return this.join(table, alias, condition, 'LEFT'); 414 | }; 415 | 416 | JoinWhereOrderLimit.prototype.right_join = function(table, alias, condition) { 417 | if (alias == null) { 418 | alias = null; 419 | } 420 | if (condition == null) { 421 | condition = null; 422 | } 423 | return this.join(table, alias, condition, 'RIGHT'); 424 | }; 425 | 426 | JoinWhereOrderLimit.prototype.outer_join = function(table, alias, condition) { 427 | if (alias == null) { 428 | alias = null; 429 | } 430 | if (condition == null) { 431 | condition = null; 432 | } 433 | return this.join(table, alias, condition, 'OUTER'); 434 | }; 435 | 436 | JoinWhereOrderLimit.prototype._joinString = function() { 437 | var j, joins, _i, _len, _ref; 438 | joins = ""; 439 | _ref = this.joins || []; 440 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 441 | j = _ref[_i]; 442 | joins += " " + j.type + " JOIN " + j.table; 443 | if (j.alias) { 444 | joins += " `" + j.alias + "`"; 445 | } 446 | if (j.condition) { 447 | joins += " ON (" + j.condition + ")"; 448 | } 449 | } 450 | return joins; 451 | }; 452 | 453 | return JoinWhereOrderLimit; 454 | 455 | })(WhereOrderLimit); 456 | 457 | Select = (function(_super) { 458 | 459 | __extends(Select, _super); 460 | 461 | function Select(options) { 462 | this.toString = __bind(this.toString, this); 463 | 464 | this.offset = __bind(this.offset, this); 465 | 466 | this.group = __bind(this.group, this); 467 | 468 | this.field = __bind(this.field, this); 469 | 470 | this.from = __bind(this.from, this); 471 | 472 | this.distinct = __bind(this.distinct, this); 473 | Select.__super__.constructor.call(this, options); 474 | this.froms = []; 475 | this.fields = []; 476 | this.groups = []; 477 | this.offsets = null; 478 | this.useDistinct = false; 479 | } 480 | 481 | Select.prototype.distinct = function() { 482 | this.useDistinct = true; 483 | return this; 484 | }; 485 | 486 | Select.prototype.from = function(table, alias) { 487 | if (alias == null) { 488 | alias = null; 489 | } 490 | table = this._sanitizeTable(table); 491 | if (alias) { 492 | alias = this._sanitizeAlias(alias); 493 | } 494 | this.froms.push({ 495 | name: table, 496 | alias: alias 497 | }); 498 | return this; 499 | }; 500 | 501 | Select.prototype.field = function(field, alias) { 502 | if (alias == null) { 503 | alias = null; 504 | } 505 | field = this._sanitizeField(field); 506 | if (alias) { 507 | alias = this._sanitizeAlias(alias); 508 | } 509 | this.fields.push({ 510 | name: field, 511 | alias: alias 512 | }); 513 | return this; 514 | }; 515 | 516 | Select.prototype.group = function(field) { 517 | field = this._sanitizeField(field); 518 | this.groups.push(field); 519 | return this; 520 | }; 521 | 522 | Select.prototype.offset = function(start) { 523 | start = this._sanitizeLimitOffset(start); 524 | this.offsets = start; 525 | return this; 526 | }; 527 | 528 | Select.prototype.toString = function() { 529 | var f, field, fields, groups, ret, table, tables, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; 530 | if (0 >= this.froms.length) { 531 | throw new Error("from() needs to be called"); 532 | } 533 | ret = "SELECT "; 534 | if (this.useDistinct) { 535 | ret += "DISTINCT "; 536 | } 537 | fields = ""; 538 | _ref = this.fields; 539 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 540 | field = _ref[_i]; 541 | if ("" !== fields) { 542 | fields += ", "; 543 | } 544 | fields += field.name; 545 | if (field.alias) { 546 | fields += " AS \"" + field.alias + "\""; 547 | } 548 | } 549 | ret += "" === fields ? "*" : fields; 550 | tables = ""; 551 | _ref1 = this.froms; 552 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 553 | table = _ref1[_j]; 554 | if ("" !== tables) { 555 | tables += ", "; 556 | } 557 | tables += table.name; 558 | if (table.alias) { 559 | tables += " `" + table.alias + "`"; 560 | } 561 | } 562 | ret += " FROM " + tables; 563 | ret += this._joinString(); 564 | ret += this._whereString(); 565 | if (0 < this.groups.length) { 566 | groups = ""; 567 | _ref2 = this.groups; 568 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 569 | f = _ref2[_k]; 570 | if ("" !== groups) { 571 | groups += ", "; 572 | } 573 | groups += f; 574 | } 575 | ret += " GROUP BY " + groups; 576 | } 577 | ret += this._orderString(); 578 | ret += this._limitString(); 579 | if (this.offsets) { 580 | ret += " OFFSET " + this.offsets; 581 | } 582 | return ret; 583 | }; 584 | 585 | return Select; 586 | 587 | })(JoinWhereOrderLimit); 588 | 589 | Update = (function(_super) { 590 | 591 | __extends(Update, _super); 592 | 593 | function Update(options) { 594 | this.toString = __bind(this.toString, this); 595 | 596 | this.set = __bind(this.set, this); 597 | 598 | this.table = __bind(this.table, this); 599 | Update.__super__.constructor.call(this, options); 600 | this.tables = []; 601 | this.fields = {}; 602 | } 603 | 604 | Update.prototype.table = function(table, alias) { 605 | if (alias == null) { 606 | alias = null; 607 | } 608 | table = this._sanitizeTable(table); 609 | if (alias) { 610 | alias = this._sanitizeAlias(alias); 611 | } 612 | this.tables.push({ 613 | name: table, 614 | alias: alias 615 | }); 616 | return this; 617 | }; 618 | 619 | Update.prototype.set = function(field, value) { 620 | field = this._sanitizeField(field); 621 | value = this._sanitizeValue(value); 622 | this.fields[field] = value; 623 | return this; 624 | }; 625 | 626 | Update.prototype.toString = function() { 627 | var field, fieldNames, fields, ret, table, tables, _i, _j, _len, _len1, _ref; 628 | if (0 >= this.tables.length) { 629 | throw new Error("table() needs to be called"); 630 | } 631 | fieldNames = (function() { 632 | var _ref, _results; 633 | _ref = this.fields; 634 | _results = []; 635 | for (field in _ref) { 636 | if (!__hasProp.call(_ref, field)) continue; 637 | _results.push(field); 638 | } 639 | return _results; 640 | }).call(this); 641 | if (0 >= fieldNames.length) { 642 | throw new Error("set() needs to be called"); 643 | } 644 | ret = "UPDATE "; 645 | tables = ""; 646 | _ref = this.tables; 647 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 648 | table = _ref[_i]; 649 | if ("" !== tables) { 650 | tables += ", "; 651 | } 652 | tables += table.name; 653 | if (table.alias) { 654 | tables += " AS `" + table.alias + "`"; 655 | } 656 | } 657 | ret += tables; 658 | fields = ""; 659 | for (_j = 0, _len1 = fieldNames.length; _j < _len1; _j++) { 660 | field = fieldNames[_j]; 661 | if ("" !== fields) { 662 | fields += ", "; 663 | } 664 | fields += "" + field + " = " + (this._formatValue(this.fields[field])); 665 | } 666 | ret += " SET " + fields; 667 | ret += this._whereString(); 668 | ret += this._orderString(); 669 | ret += this._limitString(); 670 | return ret; 671 | }; 672 | 673 | return Update; 674 | 675 | })(WhereOrderLimit); 676 | 677 | Delete = (function(_super) { 678 | 679 | __extends(Delete, _super); 680 | 681 | function Delete() { 682 | this.toString = __bind(this.toString, this); 683 | 684 | this.from = __bind(this.from, this); 685 | return Delete.__super__.constructor.apply(this, arguments); 686 | } 687 | 688 | Delete.prototype.table = null; 689 | 690 | Delete.prototype.from = function(table, alias) { 691 | table = this._sanitizeTable(table); 692 | if (alias) { 693 | alias = this._sanitizeAlias(alias); 694 | } 695 | this.table = { 696 | name: table, 697 | alias: alias 698 | }; 699 | return this; 700 | }; 701 | 702 | Delete.prototype.toString = function() { 703 | var ret; 704 | if (!this.table) { 705 | throw new Error("from() needs to be called"); 706 | } 707 | ret = "DELETE FROM " + this.table.name; 708 | if (this.table.alias) { 709 | ret += " `" + this.table.alias + "`"; 710 | } 711 | ret += this._joinString(); 712 | ret += this._whereString(); 713 | ret += this._orderString(); 714 | ret += this._limitString(); 715 | return ret; 716 | }; 717 | 718 | return Delete; 719 | 720 | })(JoinWhereOrderLimit); 721 | 722 | Insert = (function(_super) { 723 | 724 | __extends(Insert, _super); 725 | 726 | function Insert(options) { 727 | this.toString = __bind(this.toString, this); 728 | 729 | this.set = __bind(this.set, this); 730 | 731 | this.into = __bind(this.into, this); 732 | Insert.__super__.constructor.call(this, options); 733 | this.table = null; 734 | this.fields = {}; 735 | } 736 | 737 | Insert.prototype.into = function(table) { 738 | table = this._sanitizeTable(table); 739 | this.table = table; 740 | return this; 741 | }; 742 | 743 | Insert.prototype.set = function(field, value) { 744 | field = this._sanitizeField(field); 745 | value = this._sanitizeValue(value); 746 | this.fields[field] = value; 747 | return this; 748 | }; 749 | 750 | Insert.prototype.toString = function() { 751 | var field, fieldNames, fields, name, values, _i, _len; 752 | if (!this.table) { 753 | throw new Error("into() needs to be called"); 754 | } 755 | fieldNames = (function() { 756 | var _ref, _results; 757 | _ref = this.fields; 758 | _results = []; 759 | for (name in _ref) { 760 | if (!__hasProp.call(_ref, name)) continue; 761 | _results.push(name); 762 | } 763 | return _results; 764 | }).call(this); 765 | if (0 >= fieldNames.length) { 766 | throw new Error("set() needs to be called"); 767 | } 768 | fields = ""; 769 | values = ""; 770 | for (_i = 0, _len = fieldNames.length; _i < _len; _i++) { 771 | field = fieldNames[_i]; 772 | if ("" !== fields) { 773 | fields += ", "; 774 | } 775 | fields += field; 776 | if ("" !== values) { 777 | values += ", "; 778 | } 779 | values += this._formatValue(this.fields[field]); 780 | } 781 | return "INSERT INTO " + this.table + " (" + fields + ") VALUES (" + values + ")"; 782 | }; 783 | 784 | return Insert; 785 | 786 | })(QueryBuilder); 787 | 788 | _export = { 789 | expr: function() { 790 | return new Expression; 791 | }, 792 | select: function(options) { 793 | return new Select(options); 794 | }, 795 | update: function(options) { 796 | return new Update(options); 797 | }, 798 | insert: function(options) { 799 | return new Insert(options); 800 | }, 801 | "delete": function(options) { 802 | return new Delete(options); 803 | }, 804 | remove: function(options) { 805 | return new Delete(options); 806 | }, 807 | DefaultQueryBuilderOptions: DefaultQueryBuilderOptions, 808 | Cloneable: Cloneable, 809 | Expression: Expression, 810 | QueryBuilder: QueryBuilder, 811 | WhereOrderLimit: WhereOrderLimit, 812 | JoinWhereOrderLimit: JoinWhereOrderLimit, 813 | Select: Select, 814 | Update: Update, 815 | Insert: Insert, 816 | Delete: Delete 817 | }; 818 | 819 | if (typeof module !== "undefined" && module !== null) { 820 | module.exports = _export; 821 | } 822 | 823 | if (typeof window !== "undefined" && window !== null) { 824 | window.squel = _export; 825 | } 826 | 827 | squel = _export; 828 | 829 | }).call(this); 830 | -------------------------------------------------------------------------------- /server/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Handle calls from the client 3 | */ 4 | Meteor.methods({ 5 | SQLinsert: function (tableName, args) { 6 | var table = Devwik.SQL.tables[tableName]; 7 | if(table.view) { 8 | var message = "Inserting into views is not supported:" + table.view.name; 9 | console.log(message); 10 | throw new Meteor.Error(message); 11 | } 12 | try { 13 | var statement = squel.insert().into(tableName); 14 | _.each(args, function(value, key) { 15 | value = Devwik.SQL.escape(value); 16 | statement.set(key, value); 17 | }); 18 | 19 | console.log(statement.toString()); 20 | var id = Devwik.SQL.execStatement(statement.toString()); 21 | } catch (err) { 22 | console.log("Caught error:" + err); 23 | throw new Meteor.Error(err.message); 24 | } 25 | return(id.insertId); 26 | }, 27 | SQLupdate: function (tableName, args, id) { 28 | var table = Devwik.SQL.tables[tableName]; 29 | if(table.view) { 30 | var message = "Updating views is not supported::" + table.view.name; 31 | console.log(message); 32 | throw new Meteor.Error(message); 33 | } 34 | try { 35 | var statement = squel.update().table(tableName); 36 | _.each(args, function(value, key) { 37 | value = Devwik.SQL.escape(value); 38 | statement.set(key, value); 39 | }); 40 | statement.where(table.dbKey + ' = ' + id).toString(); 41 | console.log(statement.toString()); 42 | var ret = Devwik.SQL.execStatement(statement.toString()); 43 | } catch (err) { 44 | console.log("Caught error:" + err); 45 | throw new Meteor.Error(err.message); 46 | } 47 | return(ret); 48 | }, 49 | SQLremove: function (tableName, id) { 50 | var table = Devwik.SQL.tables[tableName]; 51 | if(table.view) { 52 | var message = "Deleting from views is not supported::" + table.view.name; 53 | console.log(message); 54 | throw new Meteor.Error(message); 55 | } 56 | try { 57 | id = Devwik.SQL.escape(id); 58 | var statement = squel.remove().from(tableName).where(table.dbKey + ' = ' + id).toString(); 59 | console.log(statement); 60 | var ret = Devwik.SQL.execStatement(statement); 61 | } catch (err) { 62 | console.log("Caught error:" + err); 63 | throw new Meteor.Error(err.message); 64 | } 65 | return(ret); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /server/dbconfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Configuration of the Meteor SQL Driver 3 | */ 4 | //Database properties. Change to match your site 5 | Devwik.SQL.Config ={}; 6 | Devwik.SQL.Config.database ='meteor'; 7 | Devwik.SQL.Config.user ='meteor'; 8 | Devwik.SQL.Config.host ='localhost'; 9 | Devwik.SQL.Config.password = '43b27d6bf68d30'; 10 | Devwik.SQL.Config.dbConfig = { 11 | host : Devwik.SQL.Config.host, 12 | database : Devwik.SQL.Config.database, 13 | user : Devwik.SQL.Config.user, 14 | password : Devwik.SQL.Config.password 15 | }; 16 | 17 | Devwik.SQL.Config.triggerSuffix = 'MeteorTrigger'; 18 | Devwik.SQL.Config.pollInterval = 100; //How often in ms we poll for changes in the db 19 | Devwik.SQL.Config.dbPrefix= 'meteor_';//Prefix for Meteor's tables 20 | Devwik.SQL.Config.tableCollection = Devwik.SQL.Config.dbPrefix + 'tables'; //where we keep the table sctruture 21 | Devwik.SQL.Config.dbChanges = Devwik.SQL.Config.dbPrefix + 'dbchanges';//Table that keeps track of changes 22 | -------------------------------------------------------------------------------- /server/dbinit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Meteor SQL Driver main file. Initializes the driver and sets up all the tables. 3 | */ 4 | 5 | start = new Date(); 6 | console.log('\n----------' + new Date() + ' SQL Driver Starting --------'); 7 | 8 | 9 | //Create the connection pool using the config info in dbconfig.js 10 | var pool = mysql.createPool(Devwik.SQL.Config.dbConfig); 11 | 12 | //Get the connection 13 | pool.getConnection(function(err, connection) { 14 | var query; 15 | if (err) throw err; 16 | Devwik.SQL.connection = connection;//provide global access to the connection 17 | 18 | //Get the list of tables in the db 19 | query = connection.query('show tables', function(err, result) { 20 | if (err) throw err; 21 | Fiber(function() { 22 | 23 | //Get the list of views in the db 24 | Devwik.SQL.View.getViews(); 25 | 26 | //Set up the table where we track changes to the db 27 | Devwik.SQL.dbChanges(); 28 | 29 | // Poll the table with changes 30 | Devwik.SQL.Poll(); 31 | Devwik.SQL.tables = {}; 32 | Devwik.SQL.views = {}; 33 | _.each(result, function(row){ //For each table in the db 34 | if(!(row.Tables_in_meteor === Devwik.SQL.Config.dbChanges)) { 35 | //Get the info about the table and its columns 36 | var table = new Devwik.SQL.Table(row.Tables_in_meteor); 37 | Devwik.SQL.tables[table.name] = table; 38 | console.log('loaded:' + table.name); 39 | } 40 | }); 41 | 42 | //Tell tables which views depend on them 43 | Devwik.SQL.View.tableDependencies(); 44 | 45 | Devwik.SQL.runTests(); 46 | //Meteor.publish the tables to the client 47 | Devwik.SQL.publishTables(); 48 | var elapsed = new Date() - start; 49 | console.log('----------' + new Date() + ' SQL Driver ready:' + elapsed + '--------'); 50 | }).run(); 51 | 52 | }); 53 | 54 | 55 | }); 56 | 57 | //Create the table that tracks the changes 58 | Devwik.SQL.dbChanges = function() { 59 | //type is INSERT, UPDATE or DELETE 60 | var createStatement = "\ 61 | CREATE TABLE IF NOT EXISTS `"+ Devwik.SQL.Config.dbChanges +"` (\ 62 | `cid` int not NULL AUTO_INCREMENT,\ 63 | `tableName` varchar(255) not NULL,\ 64 | `rowId` int(11) not NULL,\ 65 | `type` varchar(16) not NULL,\ 66 | `ts` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,\ 67 | PRIMARY KEY (cid)\ 68 | ) ENGINE=INNODB;"; 69 | 70 | Devwik.SQL.execStatement(createStatement); 71 | Devwik.SQL.execStatement('drop index dbchangesIndex on ' + Devwik.SQL.Config.dbChanges); 72 | var createIndex = 'create index dbchangesIndex on ' + Devwik.SQL.Config.dbChanges + '(ts)'; 73 | Devwik.SQL.execStatement(createIndex); 74 | var statement = squel.remove().from(Devwik.SQL.Config.dbChanges); 75 | Devwik.SQL.execStatement(statement.toString()); 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /server/dblib.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Misc database library functions 3 | */ 4 | 5 | /* 6 | * Execute an SQL statement. This can be both DML (queries) or DDL (create/alter) 7 | * @param {String} statement : The statement to run 8 | * @param {Boolean} throwException : If true, throw exception on error. Default:false 9 | * @returns {array} rows:The rows, if any returned by the query 10 | */ 11 | Devwik.SQL.execStatement = function(statement, transaction) { 12 | var future = new Future(); 13 | if(transaction) { 14 | console.log('tranaction'); 15 | if(transaction.cancelled) { 16 | console.log('tranaction cancelled'); 17 | //not doing anything in this transaction 18 | return([]); 19 | } 20 | } 21 | query = Devwik.SQL.connection.query(statement, function(err, result) { 22 | if (err) { 23 | if(transaction) { 24 | transaction.cancelled = true; 25 | } 26 | console.log(err); 27 | console.log(err.stack); 28 | } 29 | future.ret(result); 30 | }); 31 | return(future.wait()); 32 | }; 33 | 34 | 35 | /* 36 | * Escape an SQL statement to try and catch SQL injections 37 | */ 38 | Devwik.SQL.escape = function(statement) { 39 | statement = statement.toString();//For consistency let's convert to string 40 | statement = Devwik.SQL.connection.escape(statement).toString(); 41 | statement = statement.substring(1, statement.length-1); 42 | return(statement); 43 | }; 44 | 45 | 46 | /* 47 | * Wrap a function in an SQL Transaction 48 | * @param {Function} func: the function that performs the SQL operations 49 | * @param {Function} errFunc: optional function to call on error 50 | * 51 | */ 52 | 53 | Devwik.SQL.Transaction = function(){ 54 | var connection = Devwik.SQL.connection; 55 | if(!connection) { 56 | console.log ("No database connection"); 57 | return null; 58 | } 59 | connection.query('START TRANSACTION'); 60 | return(this); 61 | }; 62 | 63 | 64 | Devwik.SQL.Transaction.prototype.end = function() { 65 | var self = this; 66 | var connection = Devwik.SQL.connection; 67 | if(self.cancelled) { 68 | console.log('rollback'); 69 | connection.query('ROLLBACK'); 70 | } else { 71 | console.log('commit'); 72 | connection.query('COMMIT'); 73 | } 74 | }; 75 | 76 | Devwik.SQL.Transaction.prototype.commit = function() { 77 | Devwik.SQL.connection.query('COMMIT'); 78 | }; 79 | 80 | Devwik.SQL.Transaction.prototype.rollback = function() { 81 | Devwik.SQL.connection.query('ROLLBACK'); 82 | }; 83 | -------------------------------------------------------------------------------- /server/lib/lib.js: -------------------------------------------------------------------------------- 1 | Devwik = function() {}; //Provide a name space 2 | Devwik.SQL = function() {}; 3 | 4 | Future = Npm.require('fibers/future'); 5 | //Using the node.js MYSQL driver from https://github.com/felixge/node-mysql 6 | Fiber = Npm.require('fibers'); 7 | mysql = Npm.require('mysql'); 8 | mysql = Npm.require('mysql'); 9 | //based on http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ 10 | Devwik.toType = function(obj) { 11 | return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); 12 | }; 13 | -------------------------------------------------------------------------------- /server/poll.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Set up polling the db for changes. Create a trigger that inserts a row into the 4 | * dbChanges table for each each INSERT, UPDATE and DELETE 5 | */ 6 | Devwik.SQL.Poll = function() { 7 | Devwik.SQL.Poll.lastChangeId = 0;//Id of the most recent change 8 | //Clear the log of changes since we're starting fresh, and reading all 9 | //the tables from scratch 10 | Devwik.SQL.execStatement(squel.remove().from(Devwik.SQL.Config.dbChanges).toString()); 11 | Devwik.SQL.doPoll(); 12 | }; 13 | 14 | //poll the db for changes 15 | Devwik.SQL.doPoll = function() { 16 | var table, statement, row, 17 | changesStatement = squel.select().from(Devwik.SQL.Config.dbChanges).where("cid > '" + Devwik.SQL.Poll.lastChangeId + "'").toString(); 18 | //TODO: Explore failure mode. Since we're polling a lot, what happens when we fail? 19 | var changes = Devwik.SQL.execStatement(changesStatement); 20 | try { 21 | _.each(changes, function(change) { 22 | Devwik.SQL.Poll.lastChangeId = change.cid;//Id of the most recent change 23 | table = Devwik.SQL.tables[change.tableName]; 24 | switch (change.type) { 25 | case 'INSERT': 26 | case 'UPDATE': 27 | statement = 'select * from ' + change.tableName + ' where ' + 28 | table.dbKey + ' = ' + change.rowId; 29 | row = Devwik.SQL.execStatement(statement)[0]; 30 | if (row) {//Could have been deleted before we apply the insert/update 31 | if(change.type == 'INSERT') { 32 | _.each(table.handles, function (handle) { 33 | handle.added(table.name, row[table.dbKey], row); 34 | }); 35 | _.each(table.views, function (view) { 36 | Devwik.SQL.views[view].add(table.name, table.dbKey, row[table.dbKey]); 37 | }); 38 | } else { 39 | _.each(table.handles, function (handle) { 40 | handle.changed(table.name, row[table.dbKey], row); 41 | }); 42 | _.each(table.views, function (view) { 43 | Devwik.SQL.views[view].change(table.name, table.dbKey, row[table.dbKey]); 44 | }); 45 | } 46 | } 47 | break; 48 | case 'DELETE': //TODO: Fix race condition with inserts 49 | _.each(table.handles, function (handle) { 50 | handle.removed(table.name, change.rowId); 51 | }); 52 | _.each(table.views, function (view) { 53 | Devwik.SQL.views[view].remove(table.name, table.dbKey, change.rowId); 54 | }); 55 | break; 56 | default: 57 | break; 58 | } 59 | }); 60 | } catch (err) { 61 | //console.log(table); 62 | console.log(err); 63 | } 64 | Meteor.setTimeout(Devwik.SQL.doPoll, Devwik.SQL.Config.pollInterval); 65 | }; 66 | -------------------------------------------------------------------------------- /server/select.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A select statemet 3 | * * @param {String} statement: the actual select statment 4 | */ 5 | 6 | //An SQL select statement 7 | Devwik.SQL.Select = function(name, statement) { 8 | var self = this; 9 | self.name = name; 10 | self.statement = statement; 11 | self.cols = []; 12 | var future = new Future(); 13 | //Get the structure for a select 14 | Devwik.SQL.connection.query(statement, function(err, rows) { 15 | if (err) throw err; 16 | _.each(rows, function(row){ 17 | if(self.cols.length === 0) { 18 | self.setCols(row); 19 | } 20 | }); 21 | future.ret(); 22 | }, self); 23 | future.wait(); 24 | 25 | this.setPublish(); 26 | return; 27 | }; 28 | 29 | 30 | Devwik.SQL.Select.prototype.setCols = function(row) { 31 | var self = this; 32 | _.each(row, function(value, name){ 33 | var col = {}; 34 | col.name = name; 35 | col.type = Devwik.toType(value); 36 | self.cols.push(col); 37 | }); 38 | }; 39 | 40 | /* 41 | * Publish the Select to the client 42 | */ 43 | 44 | Devwik.SQL.Select.prototype.setPublish = function() { 45 | var select = this; 46 | Meteor.publish(select.name, function () { 47 | var self = this; 48 | /* 49 | * Set up the callbacks 50 | */ 51 | select.added = function(name, id, data) { 52 | self.added(name, id, data); 53 | }; 54 | select.changed = function(name, id, data) { 55 | self.changed(name, id, data); 56 | }; 57 | select.removed = function(name, id) { 58 | self.removed(name, id); 59 | }; 60 | //fut.ret(); 61 | query = Devwik.SQL.connection.query(select.statement, function(err, result) { 62 | if (err) { 63 | throw err; 64 | } 65 | _.each(result, function(row){ 66 | self.added(select.name, new Meteor.Collection.ObjectID(), row); 67 | }); 68 | self.ready();//indicate that the initial rows are ready 69 | }); 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /server/table.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * An SQL database table 4 | * * @param {String} name: the name of the table 5 | */ 6 | 7 | //An SQL database table 8 | Devwik.SQL.Table = function(name) { 9 | var self = this; 10 | self.name = name; 11 | self.cols = []; 12 | self.views = []; //Views that depend on this table 13 | var future = new Future(); 14 | //Get the structure for a table 15 | Devwik.SQL.connection.query('describe ' + name, function(err, rows) { 16 | if (err) throw err; 17 | _.each(rows, function(row){ 18 | var col = new Devwik.SQL.Column(row); 19 | if(col.dbKey) { 20 | self.dbKey = col.dbKey; 21 | } 22 | self.cols.push(col); 23 | }); 24 | if(!self.dbKey) { 25 | //Is it a view? 26 | if(Devwik.SQL.View.list[self.name]) { 27 | self.view = new Devwik.SQL.View(self.name, self); 28 | console.log(self.name + ' is a view'); 29 | } else { 30 | console.log('NO Key in:' + self.name); 31 | } 32 | } 33 | future.ret(); 34 | }, self); 35 | future.wait(); 36 | 37 | if (self.dbKey) { 38 | self.createTriggers(); 39 | self.setPublish(); 40 | } 41 | 42 | return; 43 | }; 44 | 45 | 46 | /* 47 | * A database column 48 | * @param {Objectl} prpos: the properties of the column that the driver gives us 49 | * Sample field from the driver 50 | * { Field: 'a', 51 | * Type: 'bigint(20) unsigned', 52 | * Null: 'NO', 53 | * Key: 'PRI', 54 | * Default: null, 55 | * Extra: 'auto_increment' } 56 | * Convert it into a more javascript friendly structure 57 | */ 58 | 59 | Devwik.SQL.Column = function(props) { 60 | var self = this; 61 | self.sqlProps = props; 62 | self.dbKey = false; 63 | if (props.Extra === 'auto_increment') { 64 | self.dbKey = props.Field; 65 | } else if (props.Key === 'PRI') { 66 | self.dbKey = props.Field; 67 | } 68 | self.name = props.Field; 69 | self.type = props.Type; 70 | self.Null = props.Null === 'YES'? true : false;//null is reserved so capitalize 71 | self.Default = props.Default;//default is reserved word so capitalize 72 | }; 73 | 74 | /* 75 | * Create the triggers for the table that insert a row on INSERT, UPDATE, DELETE 76 | */ 77 | 78 | Devwik.SQL.Table.prototype.createTriggers = function() { 79 | var self = this; 80 | if (self.dbKey) { 81 | //Insert Trigger 82 | var insertTriggerName = self.name + 'Insert' + Devwik.SQL.Config.triggerSuffix, 83 | dropInsertTrigger = "DROP TRIGGER " + insertTriggerName, 84 | insertTrigger = "CREATE TRIGGER " + insertTriggerName + " AFTER INSERT ON " + self.name + 85 | " FOR EACH ROW BEGIN INSERT INTO " + Devwik.SQL.Config.dbChanges + 86 | "(tableName, rowId, type) VALUES('" + self.name +"'," + 87 | "new."+ self.dbKey +"," + " 'INSERT'); END;"; 88 | 89 | Devwik.SQL.execStatement(dropInsertTrigger); 90 | Devwik.SQL.execStatement(insertTrigger); 91 | 92 | //Update Trigger 93 | var updateTriggerName = self.name + 'Update' + Devwik.SQL.Config.triggerSuffix, 94 | dropUpdateTrigger = "DROP TRIGGER " + updateTriggerName, 95 | updateTrigger = "CREATE TRIGGER " + updateTriggerName + " AFTER Update ON " + self.name + 96 | " FOR EACH ROW BEGIN INSERT INTO " + Devwik.SQL.Config.dbChanges + 97 | "(tableName, rowId, type) VALUES('" + self.name +"'," + 98 | "new."+ self.dbKey +"," + " 'UPDATE'); END;"; 99 | Devwik.SQL.execStatement(dropUpdateTrigger); 100 | Devwik.SQL.execStatement(updateTrigger); 101 | 102 | //Delete Trigger 103 | var deleteTriggerName = self.name + 'Delete' + Devwik.SQL.Config.triggerSuffix, 104 | dropDeleteTrigger = "DROP TRIGGER " + deleteTriggerName, 105 | deleteTrigger = "CREATE TRIGGER " + deleteTriggerName + " AFTER Delete ON " + self.name + 106 | " FOR EACH ROW BEGIN INSERT INTO " + Devwik.SQL.Config.dbChanges + 107 | "(tableName, rowId, type) VALUES('" + self.name +"'," + 108 | "old."+ self.dbKey +"," + " 'DELETE'); END;"; 109 | Devwik.SQL.execStatement(dropDeleteTrigger); 110 | Devwik.SQL.execStatement(deleteTrigger); 111 | 112 | } 113 | }; 114 | 115 | /* 116 | * Publish the table to the client 117 | */ 118 | 119 | Devwik.SQL.Table.prototype.setPublish = function() { 120 | var table = this; 121 | table.handles = []; 122 | // TODO: Should be wrapped in future, but hangs at this point 123 | //var fut = new Future(); 124 | // server: publish the table as a collection 125 | Meteor.publish(table.name, function () { 126 | var self = this; 127 | if (_.indexOf(table.handles, self) === -1) {//Haven't seen this one yet 128 | table.handles.push(self); //add it 129 | } 130 | 131 | self.onStop(function () {//TODO test more 132 | table.handles = _.without(table.handles, self); 133 | }); 134 | /* 135 | * Set up the callbacks 136 | */ 137 | table.added = function(name, id, data) { 138 | console.log('added:' + name); 139 | if(!table.view) { 140 | self.added(name, id, data); 141 | } else { 142 | console.log('adding'); 143 | var compositeKey = table.view.createKey(data); 144 | console.log(compositeKey); 145 | self.added(table.name, compositeKey, data); 146 | } 147 | }; 148 | table.changed = function(name, id, data) { 149 | self.changed(name, id, data); 150 | }; 151 | table.removed = function(name, id) { 152 | self.removed(name, id); 153 | }; 154 | 155 | 156 | //fut.ret(); 157 | statement = "select * from " + table.name; 158 | query = Devwik.SQL.connection.query(statement, function(err, result) { 159 | if (err) { 160 | throw err; 161 | } 162 | _.each(result, function(row){ 163 | if(!table.view) { 164 | self.added(table.name, row[table.dbKey], row); 165 | } else { 166 | //Concatenate the different keys for the view 167 | var compositeKey = table.view.createKey(row); 168 | self.added(table.name, compositeKey, row); 169 | } 170 | }); 171 | self.ready();//indicate that the initial rows are ready 172 | }); 173 | }); 174 | //return fut.wait(); 175 | }; 176 | 177 | /* 178 | * Create a collection of the Meta data of all the tables 179 | */ 180 | Devwik.SQL.publishTables = function() { 181 | Meteor.publish(Devwik.SQL.Config.tableCollection, function () { 182 | var self = this; 183 | _.each(Devwik.SQL.tables, function(table, name){ 184 | var tableProps = {}; 185 | tableProps.cols = table.cols; 186 | tableProps.dbKey = table.dbKey; 187 | tableProps.name = table.name; 188 | tableProps.type = table.type; 189 | tableProps.Null = table.Null; 190 | tableProps.Default = table.Default; 191 | self.added(Devwik.SQL.Config.tableCollection, name, tableProps); 192 | }); 193 | self.ready();//indicate that the initial rows are ready 194 | }); 195 | }; 196 | -------------------------------------------------------------------------------- /server/tests.js: -------------------------------------------------------------------------------- 1 | Devwik.SQL.runTests = function() { 2 | //Example of a select with a join 3 | var select = new Devwik.SQL.Select('empsPerCity', 'select count(*) empNumber, offices.* from employees, offices where offices.officeCode = employees.officecode group by officeCode'); 4 | //Devwik.SQL.Tests.transactions(); 5 | }; 6 | 7 | Devwik.SQL.Tests = {}; 8 | 9 | Devwik.SQL.Tests.transactions = function() { 10 | var transaction = new Devwik.SQL.Transaction(); 11 | if(transaction) { 12 | console.log(Devwik.SQL.execStatement('select count(*) from employees', 13 | transaction)[0]); 14 | //employeeNumber is a unique key at least one of these should fail 15 | Devwik.SQL.execStatement("INSERT INTO employees (employeeNumber,firstName, lastName, email, jobTitle) VALUES (1759, 'aaaa', 'bbb', 'ddd', 'ccc')", transaction); 16 | Devwik.SQL.execStatement("INSERT INTO employees (employeeNumber,firstName, lastName, email, jobTitle) VALUES (1759, 'aaaa', 'bbb', 'ddd', 'ccc')", transaction); 17 | transaction.end(); 18 | } 19 | console.log(Devwik.SQL.execStatement('select count(*) from employees')[0]); 20 | }; 21 | -------------------------------------------------------------------------------- /server/view.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * An SQL database view 4 | * * @param {String} name: the name of the view 5 | * * @param {Object} name: the table object for the view 6 | * 7 | * We currently only support simple views in terms of reactivity: 8 | * Each table in the view needs to have a unique key 9 | * There should be no aggregates in the view 10 | * We don't support updatable views 11 | * 12 | * Strategy 13 | * -------- 14 | * 15 | * Views let each table in the view know that they're dependent on the table. 16 | * When the table is changed: insert, update, delete, it calls the view and lets 17 | * it know which row was affected. The view then handles the row(s). 18 | * Deleting rows is more complicated since once the row is deleted in the db, 19 | * we don't know which rows have been affected in the view. We therefor create a temp 20 | * table on startup where we keep all the keys to the view. When there's a delete, 21 | * we look in the temp table to see which rows were affected. 22 | * 23 | * Views and table have a complicated relationship: 24 | * 1. A view is a kind of table. So it's both a table object and a View object. 25 | * The table object points to the view object: table.view and vice versa, the 26 | * view object points to the table object view.table. 27 | * 2. The View object has a link of the tables that the view depends on in 28 | * view.tables. Whenever one of these tables changes the view changes too. 29 | * 3. Each table that's not a view has a list of views that are affected by 30 | * it. This list could be empty. Thi is the reverse of 2 above. 31 | */ 32 | 33 | //An SQL database view 34 | Devwik.SQL.View = function(name, table) { 35 | var self = this, 36 | row = Devwik.SQL.View.list[name]; 37 | self.name = name; 38 | self.table = table; 39 | self.tables = []; //List of tables this view depends on 40 | self.dbKeys = []; //List of keys this view depends on 41 | self.updatable = row.IS_UPDATABLE; 42 | self.query = row.VIEW_DEFINITION; 43 | 44 | Fiber(function() { 45 | //Now let's find the list of tables affected 46 | var explain = 'explain ' + self.query; 47 | var infoRows = Devwik.SQL.execStatement(explain); 48 | _.each(infoRows, function(infoRow){ //For each table in the db 49 | self.tables.push(infoRow.table); 50 | }); 51 | }).run(); 52 | 53 | Devwik.SQL.views[name] = self; 54 | }; 55 | 56 | Devwik.SQL.View.list = {}; 57 | 58 | /* 59 | * Add rows to a view. Doesn't add any data to the db. Just 60 | * Queries the view to figure out which rows have been added to it. 61 | */ 62 | Devwik.SQL.View.prototype.add = function(tableName, key, id) { 63 | var self = this; 64 | var statement = squel.select().from(this.name).where(key + " = '" + id + "'").toString(); 65 | var table = self.table; 66 | rows = Devwik.SQL.execStatement(statement); 67 | _.each(rows, function(row){ //For each row affected 68 | _.each(table.handles, function (handle) {//Each client listening 69 | var key = self.createKey(row); 70 | handle.added(table.name, key, row); 71 | }); 72 | }); 73 | }; 74 | 75 | /* 76 | * Change rows to a view. Doesn't change any data in the db. Just 77 | * Queries the view to figure out which rows have been changed. 78 | */ 79 | Devwik.SQL.View.prototype.change = function(tableName, key, id) { 80 | var self = this; 81 | var statement = squel.select().from(this.name).where(key + " = '" + id + "'").toString(); 82 | var table = self.table; 83 | rows = Devwik.SQL.execStatement(statement); 84 | _.each(rows, function(row){ //For each row affected 85 | _.each(table.handles, function (handle) {//Each client listening 86 | var key = self.createKey(row); 87 | handle.changed(table.name, key, row); 88 | }); 89 | }); 90 | }; 91 | 92 | /* 93 | * Delete rows in a view. Doesn't change any data in the db. Just 94 | * Queries the view to figure out which rows have been deleted. 95 | */ 96 | Devwik.SQL.View.prototype.remove = function(tableName, key, id) { 97 | var self = this; 98 | var table = self.table; 99 | //Need to go to the table where we keep the keys and figure out what got deleted 100 | var select = squel.select().from(self.tmpName).where(key + " = '" + id + "'"); 101 | //For each of the affected rows 102 | var rows = Devwik.SQL.execStatement(select.toString()); 103 | _.each(rows, function(row){ //For each row affected 104 | _.each(table.handles, function (handle) {//Each client listening 105 | var key = self.createKey(row); 106 | handle.removed(table.name, key, row); 107 | //TODO: remove from the temp table once we have transactions 108 | }); 109 | }); 110 | }; 111 | 112 | /* 113 | * Create a temp table with the rows in the view 114 | */ 115 | Devwik.SQL.View.prototype.saveKeys = function() { 116 | var self = this; 117 | self.tmpName = Devwik.SQL.Config.dbPrefix + 'tmp_' + self.name; 118 | var drop = 'drop table if exists ' + self.tmpName; 119 | Devwik.SQL.execStatement(drop); 120 | var select = squel.select().from(self.name); 121 | _.each(self.dbKeys, function(key){ 122 | select.field(key); 123 | }); 124 | var create = 'create temporary table ' + self.tmpName + ' as ' + select.toString(); 125 | Devwik.SQL.execStatement(create); 126 | }; 127 | 128 | /* 129 | * Create a composite key based on the individual keys in the table 130 | */ 131 | Devwik.SQL.View.prototype.createKey = function(row) { 132 | var self = this; 133 | var compositeKey = ''; 134 | var separator = ''; 135 | _.each(self.dbKeys, function(key){ 136 | compositeKey += separator + row[key]; 137 | separator = '-'; 138 | }); 139 | return (compositeKey); 140 | }; 141 | 142 | /* 143 | * Tell tables which views depend on them 144 | */ 145 | Devwik.SQL.View.tableDependencies = function() { 146 | //For each views, find the tables 147 | _.each(Devwik.SQL.views, function(view) { 148 | //For each table 149 | _.each(view.tables, function(table) { 150 | var currTable = Devwik.SQL.tables[table]; 151 | currTable.views.push(view.name); 152 | var keyName = Devwik.SQL.tables[table].dbKey; 153 | view.dbKeys.push(keyName); 154 | }); 155 | view.saveKeys(); 156 | view.table.setPublish(); 157 | }); 158 | }; 159 | 160 | /* 161 | * Get the list of views from the db 162 | */ 163 | Devwik.SQL.View.getViews = function(name) { 164 | var statement = squel.select().from('INFORMATION_SCHEMA.VIEWS').toString(); 165 | var rows = Devwik.SQL.execStatement(statement); 166 | _.each(rows, function(row){ //For each table in the db 167 | Devwik.SQL.View.list[row.TABLE_NAME] = row; 168 | }); 169 | }; 170 | 171 | --------------------------------------------------------------------------------