├── .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 |
2 |
3 |
4 |
Table handling
5 | Database tables are detected automatically, you need do nothing on the server.
6 | On the client:
7 |
var Employee = new Meteor.Table('employees');
8 | Defines the Table object. and you use it:
9 |
Template.employeeRows.employees = function () {
10 | return Employee.find({}, {sort: {employeeNumber: -1}});
11 | };
12 | Notice that the on the client we're still using minimongo not SQL.
13 |
14 |
15 |
41 |
42 | {{> employeeRows}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Id
52 |
53 |
54 | First
55 |
56 |
57 | Last
58 |
59 |
60 | Title
61 |
62 |
63 | Email
64 |
65 |
66 |
67 | {{#each employees}}
68 | {{> employee}}
69 | {{/each}}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
78 |
79 | delete
80 |
81 |
82 | {{employeeNumber}}
83 |
84 |
85 | {{firstName}}
86 |
87 |
88 | {{lastName}}
89 |
90 |
91 | {{jobTitle}}
92 |
93 |
94 | {{email}}
95 |
96 |
97 |
98 |
99 |
100 |
101 | {{employeeNumber}}
102 |
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 |
2 |
3 | The database schema is automatically generated. Each table has to have a unique key to be included.
4 |
5 |
6 | {{#each tables}}
7 | {{> devwikTable}}
8 | {{/each}}
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Name
24 |
25 |
26 | Type
27 |
28 |
29 | Null
30 |
31 |
32 | Default
33 |
34 |
35 |
36 |
37 | {{#each cols}}
38 | {{> devwikCol}}
39 | {{/each}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {{name}}
51 |
52 |
53 | {{type}}
54 |
55 |
56 | {{Null}}
57 |
58 |
59 | {{Default}}
60 |
61 |
62 |
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 |
2 |
3 |
4 |
Selects are currently not reactive. but you can use any select statment including aggregates, sub-selects, etc.
5 | On the server:
6 |
new Devwik.SQL.Select('empsCities', 'select employees.*, offices.city from employees, offices where offices.officeCode = employees.officecode');
7 |
8 | On the client:
9 |
var Select = new Meteor.Select('empsCities');
10 | And then just use standard Meteor operations:
11 |
Template.devwikSelects.selects = function () {
12 | return Select.find({});
13 | };
14 |
15 |
16 |
17 |
18 |
19 | Number of Employees
20 |
21 |
22 | City
23 |
24 |
25 | State
26 |
27 |
28 | Country
29 |
30 |
31 |
32 | {{#each selects}}
33 | {{> select}}
34 | {{/each}}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{empNumber}}
42 |
43 |
44 | {{city}}
45 |
46 |
47 | {{state}}
48 |
49 |
50 | {{country}}
51 |
52 |
53 |
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 |
2 |
3 |
4 |
5 |
6 | Currently you can only use
simple views that include the keys from the original tables but they are
reactive. Any chages to the underlying tables is automatically propagated to the view.
7 |
8 |
create view bar as select firstName, lastName, email, jobTitle, employees.officeCode, city, addressLine1, state, country from offices, employees where employees.officeCode = offices.officeCode limit 3;
9 | works fine. It gives us employee and office info for each employee and includes keys in each table.
10 | 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 in the client using the view name.
11 |
var empCity = new Meteor.Table('empCity');
12 |
Updatable views are not supported. You can't insert, update or delete from a view.
13 |
14 |
15 |
16 |
17 |
18 | City
19 |
20 |
21 | Id
22 |
23 |
24 | First
25 |
26 |
27 | Last
28 |
29 |
30 | Title
31 |
32 |
33 | Email
34 |
35 |
36 |
37 | {{#each viewRows}}
38 | {{> viewRow}}
39 | {{/each}}
40 |
41 |
42 |
43 |
44 |
45 |
46 | {{city}}
47 |
48 |
49 | {{employeeNumber}}
50 |
51 |
52 | {{firstName}}
53 |
54 |
55 | {{lastName}}
56 |
57 |
58 | {{jobTitle}}
59 |
60 |
61 | {{email}}
62 |
63 |
64 |
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 |
--------------------------------------------------------------------------------