├── .gitignore
├── .meteor
├── release
├── .gitignore
├── platforms
├── .finished-upgraders
├── packages
├── .id
└── versions
├── packages
└── meteor-postgres
│ ├── lib
│ ├── init.js
│ ├── client.coffee
│ ├── collection.coffee
│ ├── sql.coffee
│ └── server.coffee
│ ├── tests
│ ├── db-settings.pg.json
│ └── jasmine
│ │ ├── collectionSpec.coffee
│ │ ├── client
│ │ └── clientSpec.coffee
│ │ └── server
│ │ └── serverSpec.coffee
│ ├── package.js
│ └── README.md
├── .versions
├── simple-todo.html
├── README.md
├── simple-todo.css
└── simple-todo.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .npm
2 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.1.0.2
2 |
--------------------------------------------------------------------------------
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 | postgresdb
3 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/lib/init.js:
--------------------------------------------------------------------------------
1 | SQL = {}
2 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/tests/db-settings.pg.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 5439
3 | }
4 |
--------------------------------------------------------------------------------
/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 |
--------------------------------------------------------------------------------
/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-platform
8 | insecure
9 | storeness:meteor-postgres
10 |
--------------------------------------------------------------------------------
/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 26kl8oltc4t01liqtc3
8 |
--------------------------------------------------------------------------------
/.versions:
--------------------------------------------------------------------------------
1 | base64@1.0.3
2 | binary-heap@1.0.3
3 | blaze@2.1.2
4 | blaze-tools@1.0.3
5 | callback-hook@1.0.3
6 | check@1.0.5
7 | ddp@1.1.0
8 | deps@1.0.7
9 | ejson@1.0.6
10 | geojson-utils@1.0.3
11 | html-tools@1.0.4
12 | htmljs@1.0.4
13 | id-map@1.0.3
14 | jquery@1.11.3_2
15 | json@1.0.3
16 | local-test:storeness:meteor-postgres@0.2.0
17 | logging@1.0.7
18 | meteor@1.1.6
19 | minifiers@1.1.5
20 | minimongo@1.0.8
21 | mongo@1.1.0
22 | observe-sequence@1.0.6
23 | ordered-dict@1.0.3
24 | random@1.0.3
25 | reactive-var@1.0.5
26 | retry@1.0.3
27 | spacebars@1.0.6
28 | spacebars-compiler@1.0.6
29 | storeness:meteor-postgres@0.2.0
30 | templating@1.1.1
31 | test-helpers@1.0.4
32 | tinytest@1.0.5
33 | tracker@1.0.7
34 | underscore@1.0.3
35 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/tests/jasmine/collectionSpec.coffee:
--------------------------------------------------------------------------------
1 | describe 'SQL.Collection', ->
2 |
3 | describe 'initialize', ->
4 |
5 | it 'throws an error if not constructed with `new`', ->
6 | expect( -> SQL.Collection(null)).toThrow(new Error 'Use new to construct a SQL.Collection')
7 |
8 | it 'throws an error if first argument does not exist', ->
9 | expect( -> new SQL.Collection()).toThrow(new Error 'First argument to new SQL.Collection must exist')
10 |
11 | it 'throws an error if first argument is not a string', ->
12 | expect( -> new SQL.Collection(123)).toThrow(new Error 'First argument to new SQL.Collection must be a string or null')
13 | expect( -> new SQL.Collection([])).toThrow(new Error 'First argument to new SQL.Collection must be a string or null')
14 | expect( -> new SQL.Collection({})).toThrow(new Error 'First argument to new SQL.Collection must be a string or null')
15 | expect( -> new SQL.Collection('123')).not.toThrow()
16 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | agershun:alasql@0.2.0
2 | autoupdate@1.2.1
3 | base64@1.0.3
4 | binary-heap@1.0.3
5 | blaze@2.1.2
6 | blaze-tools@1.0.3
7 | boilerplate-generator@1.0.3
8 | callback-hook@1.0.3
9 | check@1.0.5
10 | coffeescript@1.0.6
11 | ddp@1.1.0
12 | deps@1.0.7
13 | ejson@1.0.6
14 | fastclick@1.0.3
15 | geojson-utils@1.0.3
16 | html-tools@1.0.4
17 | htmljs@1.0.4
18 | http@1.1.0
19 | id-map@1.0.3
20 | insecure@1.0.3
21 | jquery@1.11.3_2
22 | json@1.0.3
23 | launch-screen@1.0.2
24 | livedata@1.0.13
25 | logging@1.0.7
26 | meteor@1.1.6
27 | meteor-platform@1.2.2
28 | minifiers@1.1.5
29 | minimongo@1.0.8
30 | mobile-status-bar@1.0.3
31 | mongo@1.1.0
32 | observe-sequence@1.0.6
33 | ordered-dict@1.0.3
34 | random@1.0.3
35 | reactive-dict@1.1.0
36 | reactive-var@1.0.5
37 | reload@1.1.3
38 | retry@1.0.3
39 | routepolicy@1.0.5
40 | session@1.1.0
41 | spacebars@1.0.6
42 | spacebars-compiler@1.0.6
43 | storeness:meteor-postgres@0.2.0
44 | templating@1.1.1
45 | tracker@1.0.7
46 | ui@1.0.6
47 | underscore@1.0.3
48 | url@1.0.4
49 | webapp@1.2.0
50 | webapp-hashing@1.0.3
51 |
--------------------------------------------------------------------------------
/simple-todo.html:
--------------------------------------------------------------------------------
1 |
2 | Todo List
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Todo List
11 |
16 |
26 |
27 |
28 |
29 | {{#each tasks}}
30 | {{> task}}
31 | {{/each}}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {{text}} - {{name}}
44 |
45 |
46 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/package.js:
--------------------------------------------------------------------------------
1 | Package.describe({
2 | name: 'storeness:meteor-postgres',
3 | version: '0.2.2',
4 | summary: 'PostgreSQL support for Meteor',
5 | git: 'https://github.com/storeness/meteor-postgres',
6 | documentation: 'README.md'
7 | });
8 |
9 | Npm.depends({
10 | 'pg': '4.3.0'
11 | });
12 |
13 | Package.onUse(function (api) {
14 | api.versionsFrom('1.1.0.2');
15 | api.use('coffeescript');
16 | api.use('underscore');
17 | api.use('tracker');
18 | api.use('ddp');
19 | api.use('agershun:alasql@0.2.0');
20 |
21 | api.addFiles([
22 | 'lib/init.js',
23 | 'lib/sql.coffee'
24 | ], ['client', 'server']);
25 |
26 | api.addFiles([
27 | 'lib/client.coffee'
28 | ], 'client');
29 |
30 | api.addFiles([
31 | 'lib/server.coffee'
32 | ], 'server');
33 |
34 | api.addFiles([
35 | 'lib/collection.coffee'
36 | ]);
37 |
38 | api.export('SQL');
39 | });
40 |
41 | Package.onTest(function (api) {
42 | api.use('sanjo:jasmine@0.15.1');
43 | api.use('coffeescript');
44 | api.use('spacebars');
45 | api.use('underscore');
46 | api.use('storeness:meteor-postgres');
47 |
48 | // Start postgres test-server
49 | api.use('numtel:pg-server');
50 | api.addFiles('tests/db-settings.pg.json');
51 |
52 | api.addFiles([
53 | 'tests/jasmine/collectionSpec.coffee'
54 | ]);
55 |
56 | api.addFiles([
57 | 'tests/jasmine/client/clientSpec.coffee'
58 | ], 'client');
59 |
60 | api.addFiles([
61 | 'tests/jasmine/server/serverSpec.coffee'
62 | ], 'server');
63 | });
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PostgreSQL + Meteor
2 |
3 | Adds postgres support to Meteor via `SQL.Collection`, which is similar to
4 | `Mongo.Collection` and provides the same functionality namely livequery, pub/sub, latency compensation and client side cache.
5 |
6 | Still I would not recommend using it in production yet as the ORM layer is open for SQL-injections. You can check out [this SQL package blueprint](https://github.com/storeness/sql) for a
7 | possible more sophisticated SQL implementation that could support most popular SQL
8 | Databases, Models and Migrations.
9 |
10 | ### Improvements
11 |
12 | - Tests
13 | - Proper support for IDs (including strings)
14 | - Cleaner code and API
15 | - Support of underscores in table and column names
16 | - Many bug fixes including
17 | - errors on creating existent tables (convenient for server startup)
18 | - postgres client event leak
19 | - alasql column bug
20 | - Working example
21 |
22 | ### Installation
23 |
24 | Run the following from your command line.
25 |
26 | ```
27 | meteor add storeness:meteor-postgres
28 | ```
29 |
30 | or add this to your `package.js`
31 |
32 | ```
33 | api.use('storeness:meteor-postgres');
34 | ```
35 |
36 | ### Usage
37 |
38 | To get started you might want to take a look at the [todo-example
39 | code](https://github.com/storeness/meteor-postgres/blob/simple-todo.js). You can run
40 | the code by cloning this repo locally and start it by running
41 | `MP_POSTGRES=postgres://{username}:{password}@{host}:{port}/{database_name}
42 | meteor` inside the cloned directory.
43 |
44 | ### Tests
45 |
46 | To run the test execute the following from your command line.
47 |
48 | ```
49 | MP_POSTGRES=postgres://{YOUR USERNAME ON THE MACHINE}:numtel@localhost:5439/postgres JASMINE_SERVER_UNIT=1 VELOCITY_TEST_PACKAGES=1 meteor --port 4000 test-packages --driver-package velocity:html-reporter storeness:meteor-postgres
50 | ```
51 |
52 | and check on [localhost:4000](http://localhost:4000)
53 |
54 | ### Implementation
55 |
56 | We use [Node-Postgres](https://github.com/brianc/node-postgres) on the server and [AlaSQL](https://github.com/agershun/alasql) on the client.
57 | Also thanks to [Meteor-Postgres](http://www.meteorpostgres.com/) as this project
58 | is based on their initial work, but which gets not longer maintained.
59 |
60 | ### License
61 |
62 | Released under the MIT license. See the LICENSE file for more info.
63 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/README.md:
--------------------------------------------------------------------------------
1 | # PostgreSQL + Meteor
2 |
3 | Adds postgres support to Meteor via `SQL.Collection`, which is similar to
4 | `Mongo.Collection` and provides the same functionality namely livequery, pub/sub, latency compensation and client side cache.
5 |
6 | Still I would not recommend using it in production yet as the ORM layer is open for SQL-injections. You can check out [this SQL package blueprint](https://github.com/storeness/sql) for a
7 | possible more sophisticated SQL implementation that could support most popular SQL
8 | Databases, Models and Migrations.
9 |
10 | ### Improvements
11 |
12 | - Tests
13 | - Proper support for IDs (including strings)
14 | - Cleaner code and API
15 | - Support of underscores in table and column names
16 | - Many bug fixes including
17 | - errors on creating existent tables (convenient for server startup)
18 | - postgres client event leak
19 | - alasql column bug
20 | - Working example
21 |
22 | ### Installation
23 |
24 | Run the following from your command line.
25 |
26 | ```
27 | meteor add storeness:meteor-postgres
28 | ```
29 |
30 | or add this to your `package.js`
31 |
32 | ```
33 | api.use('storeness:meteor-postgres');
34 | ```
35 |
36 | ### Usage
37 |
38 | To get started you might want to take a look at the [todo-example
39 | code](https://github.com/storeness/meteor-postgres/blob/simple-todo.js). You can run
40 | the code by cloning this repo locally and start it by running
41 | `MP_POSTGRES=postgres://{username}:{password}:{url}:{port}/{database_name}
42 | meteor` inside the cloned directory.
43 |
44 | ### Tests
45 |
46 | To run the test execute the following from your command line.
47 |
48 | ```
49 | MP_POSTGRES=postgres://{YOUR USERNAME ON THE MACHINE}:numtel@localhost:5439/postgres JASMINE_SERVER_UNIT=1 VELOCITY_TEST_PACKAGES=1 meteor --port 4000 test-packages --driver-package velocity:html-reporter storeness:meteor-postgres
50 | ```
51 |
52 | and check on [localhost:4000](http://localhost:4000)
53 |
54 | ### Implementation
55 |
56 | We use [Node-Postgres](https://github.com/brianc/node-postgres) on the server and [AlaSQL](https://github.com/agershun/alasql) on the client.
57 | Also thanks to [Meteor-Postgres](http://www.meteorpostgres.com/) as this project
58 | is based on their initial work, but which gets not longer maintained.
59 |
60 | ### License
61 |
62 | Released under the MIT license. See the LICENSE file for more info.
63 |
--------------------------------------------------------------------------------
/simple-todo.css:
--------------------------------------------------------------------------------
1 | /* CSS declarations go here */
2 | body {
3 | font-family: sans-serif;
4 | background-color: #315481;
5 | background-image: linear-gradient(to bottom, #315481, #918e82 100%);
6 | background-attachment: fixed;
7 |
8 | position: absolute;
9 | top: 0;
10 | bottom: 0;
11 | left: 0;
12 | right: 0;
13 |
14 | padding: 0;
15 | margin: 0;
16 |
17 | font-size: 14px;
18 | }
19 |
20 | .container {
21 | max-width: 600px;
22 | margin: 0 auto;
23 | min-height: 100%;
24 | background: white;
25 | }
26 |
27 | header {
28 | background: #d2edf4;
29 | background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
30 | padding: 20px 15px 15px 15px;
31 | position: relative;
32 | }
33 |
34 | #login-buttons {
35 | display: block;
36 | }
37 |
38 | h1 {
39 | font-size: 1.5em;
40 | margin: 0;
41 | margin-bottom: 10px;
42 | display: inline-block;
43 | margin-right: 1em;
44 | }
45 |
46 | form {
47 | margin-top: 10px;
48 | margin-bottom: -10px;
49 | position: relative;
50 | }
51 |
52 | .new-user input {
53 | box-sizing: border-box;
54 | padding: 10px 0;
55 | background: transparent;
56 | border: none;
57 | width: 100%;
58 | padding-right: 80px;
59 | font-size: 1em;
60 | }
61 |
62 | .new-user input:focus{
63 | outline: 0;
64 | }
65 |
66 | .new-task input {
67 | box-sizing: border-box;
68 | padding: 10px 0;
69 | background: transparent;
70 | border: none;
71 | width: 100%;
72 | padding-right: 80px;
73 | font-size: 1em;
74 | }
75 |
76 | .new-task input:focus{
77 | outline: 0;
78 | }
79 |
80 | ul {
81 | margin: 0;
82 | padding: 0;
83 | background: white;
84 | }
85 |
86 | .delete {
87 | float: right;
88 | font-weight: bold;
89 | background: none;
90 | font-size: 1em;
91 | border: none;
92 | position: relative;
93 | }
94 |
95 | li {
96 | position: relative;
97 | list-style: none;
98 | padding: 15px;
99 | border-bottom: #eee solid 1px;
100 | }
101 |
102 | li .text {
103 | margin-left: 10px;
104 | }
105 |
106 | li.checked {
107 | color: #888;
108 | }
109 |
110 | li.checked .text {
111 | text-decoration: line-through;
112 | }
113 |
114 | li.private {
115 | background: #eee;
116 | border-color: #ddd;
117 | }
118 |
119 | header .hide-completed {
120 | float: right;
121 | }
122 |
123 | .toggle-private {
124 | margin-left: 5px;
125 | }
126 |
127 | @media (max-width: 600px) {
128 | li {
129 | padding: 12px 15px;
130 | }
131 |
132 | .search {
133 | width: 150px;
134 | clear: both;
135 | }
136 |
137 | .new-task input {
138 | padding-bottom: 5px;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/lib/client.coffee:
--------------------------------------------------------------------------------
1 | SQL.Client = (Collection) ->
2 | Collection = Collection or Object.create SQL.Client::
3 | Collection.table = Collection.tableName
4 |
5 | Collection.tableElements = {}
6 | SQL.Client::clearAll()
7 | Collection
8 |
9 |
10 | # Load all the shared SQL methods
11 | _.extend SQL.Client::, SQL.Sql::
12 |
13 | SQL.Client::createTable = (tableObj) ->
14 | alasql.fn.Date = Date
15 | startString = "CREATE TABLE IF NOT EXISTS #{@table} ("
16 | item = undefined
17 | subKey = undefined
18 | valOperator = undefined
19 | inputString = ''
20 |
21 | for key of tableObj
22 | @tableElements[key] = key
23 | inputString += " #{key} "
24 | inputString += @_DataTypes[tableObj[key][0]]
25 | if _.isArray(tableObj[key]) and tableObj[key].length > 1
26 | i = 1
27 | count = tableObj[key].length
28 | while i < count
29 | item = tableObj[key][i]
30 | if _.isObject item
31 | subKey = Object.keys item
32 | inputString += " #{@_TableConstraints[subKey]}#{item[subKey]}"
33 | else
34 | inputString += " #{@_TableConstraints[item]}"
35 | i++
36 | inputString += ', '
37 |
38 | # check to see if id already provided
39 | startString += 'id varchar(255) primary key,' if inputString.indexOf(' id') is -1
40 |
41 | @inputString = "#{startString}#{inputString} created_at Date);"
42 | @prevFunc = 'CREATE TABLE'
43 | # create the table
44 | alasql @inputString, @dataArray
45 | @clearAll()
46 | return
47 |
48 | SQL.Client::fetch = (server) ->
49 | @reactiveData?.depend()
50 |
51 | starter = @updateString or @deleteString or @selectString
52 | input = if @inputString.length > 0 then @inputString else starter + @joinString + @whereString + @orderString + @limitString + @offsetString + @groupString + @havingString + ';'
53 |
54 | try
55 | result = alasql(input, @dataArray)
56 | catch e
57 | @clearAll()
58 |
59 | if server is 'server'
60 | input = if @inputString.length > 0 then @inputString else starter + @joinString + @whereString + @orderString + @limitString + @offsetString + @groupString + @havingString + ';'
61 | Meteor.call "#{@table}_fetch", @_convertQueryForServer(input), @dataArray
62 | @clearAll()
63 | result
64 |
65 | SQL.Client::save = (client) ->
66 | starter = @updateString or @deleteString or @selectString
67 | input = if @inputString.length > 0 then @inputString else starter + @joinString + @whereString + ';'
68 |
69 | try
70 | result = alasql(input, @dataArray)
71 | catch e
72 | @clearAll()
73 |
74 | unless client is 'client'
75 | input = if @inputString.length > 0 then @inputString else starter + @joinString + @whereString + ';'
76 | @unvalidated = true
77 | Meteor.call "#{@table}_save", @_convertQueryForServer(input), @dataArray
78 |
79 | @reactiveData.changed() if @reactiveData
80 | @clearAll()
81 | result
82 |
83 | SQL.Client::_convertQueryForServer = (input) ->
84 | counter = 1
85 | _.map(input.split(''), (character) -> if character is "?" then "$#{counter++}" else character).join('')
86 |
--------------------------------------------------------------------------------
/simple-todo.js:
--------------------------------------------------------------------------------
1 | // Defining 2 SQL collections. The additional paramater is the postgres connection string which will only run on the server
2 | tasks = new SQL.Collection('tasks');
3 | username = new SQL.Collection('username');
4 |
5 | if (Meteor.isClient) {
6 | var newUser = 'all';
7 | var taskTable = {
8 | id: ['$number'],
9 | text: ['$string', '$notnull'],
10 | checked: ['$bool'],
11 | usernameid: ['$number']
12 | };
13 |
14 | tasks.createTable(taskTable);
15 |
16 | var usersTable = {
17 | id: ['$number'],
18 | name: ['$string', '$notnull']
19 | };
20 | username.createTable(usersTable);
21 |
22 |
23 | Template.body.helpers({
24 | usernames: function () {
25 | return username.select().fetch();
26 | },
27 | tasks: function () {
28 | if (newUser === 'all'){
29 | return tasks.select('tasks.id', 'tasks.text', 'tasks.checked', 'tasks.created_at', 'username.name')
30 | .join(['OUTER JOIN'], ['usernameid'], [['username', ['id']]])
31 | .fetch();
32 | }
33 | else {
34 | return tasks.select('tasks.id', 'tasks.text', 'tasks.checked', 'tasks.created_at', 'username.name')
35 | .join(['OUTER JOIN'], ['usernameid'], [['username', ['id']]])
36 | .where("name = ?", newUser)
37 | .fetch();
38 | }
39 | }
40 | });
41 |
42 |
43 | Template.body.events({
44 | "submit .new-task": function (event) {
45 | if (event.target.category.value){
46 | var user = username.select()
47 | .where("name = ?", event.target.category.value)
48 | .fetch();
49 | user = user[0].id;
50 | var text = event.target.text.value;
51 | tasks.insert({
52 | text:text,
53 | checked:false,
54 | usernameid: user
55 | }).save();
56 | event.target.text.value = "";
57 | } else{
58 | alert("please add a user first");
59 | }
60 | return false;
61 | },
62 | "submit .new-user": function (event) {
63 | var text = event.target.text.value;
64 | username.insert({
65 | name:text
66 | }).save();
67 | event.target.text.value = "";
68 |
69 | return false;
70 | },
71 | "click .toggle-checked": function () {
72 | tasks.update({id: this.id, "checked": !this.checked})
73 | .where("id = ?", this.id)
74 | .save();
75 | },
76 | "click .delete": function () {
77 | tasks.remove()
78 | .where("id = ?", this.id)
79 | .save();
80 | },
81 | "change .catselect": function(event){
82 | newUser = event.target.value;
83 | tasks.reactiveData.changed();
84 | }
85 | });
86 | }
87 |
88 | if (Meteor.isServer) {
89 | tasks.createTable({text: ['$string'], checked: ["$bool", {'$default': false}], usernameid: ['$string']});
90 | username.createTable({name: ['$string', '$unique']});
91 |
92 | username.insert({name:'all'}).save();
93 |
94 | tasks.publish('tasks', function(){
95 | return tasks.select('tasks.id as id', 'tasks.text', 'tasks.checked', 'tasks.created_at', 'username.id as usernameid', 'username.name')
96 | .join(['INNER JOIN'], ["usernameid"], [["username", 'id']])
97 | .order('created_at DESC')
98 | .limit(100);
99 | });
100 |
101 | username.publish('username', function(){
102 | return username.select('id', 'name')
103 | .order('created_at DESC')
104 | .limit(100);
105 | });
106 | }
107 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/tests/jasmine/client/clientSpec.coffee:
--------------------------------------------------------------------------------
1 | describe 'SQL.Server', ->
2 |
3 | tableTestTasks =
4 | text: ['$string', '$notnull']
5 |
6 | tableTestUsers =
7 | username: ['$string', '$notnull']
8 | age: ['$number']
9 |
10 | sqlStub = (name) ->
11 | stub = SQL.Client()
12 | stub.table = name
13 | stub
14 |
15 | testTasks = sqlStub 'test_tasks'
16 | testUsers = sqlStub 'test_users'
17 |
18 | beforeEach (done) ->
19 | try
20 | testTasks.dropTable().save()
21 | testUsers.dropTable().save()
22 | catch e
23 | console.error e
24 |
25 | testTasks.createTable(tableTestTasks)
26 | _(3).times (n) -> testTasks.insert({ id: "#{n+1}", text: "testing#{n + 1}" }).save()
27 | _(5).times (n) -> testTasks.insert({ id: "#{n+1+3}", text: "testing1" }).save()
28 |
29 |
30 | testUsers.createTable(tableTestUsers)
31 | _(3).times (n) ->
32 | testUsers.insert({ id: "#{n*2+1}", username: "eddie#{n + 1}", age: 2 * n }).save()
33 | _(3).times (n) ->
34 | testUsers.insert({ id: "#{n*2+2}", username: "paulo", age: 27 }).save()
35 | done()
36 |
37 | describe 'createTable', ->
38 |
39 | it 'has string IDs', ->
40 | console.log testTasks.select().fetch()
41 | result = testTasks.findOne().fetch()
42 | expect(result[0].id).toEqual(jasmine.any(String))
43 |
44 | describe 'fetch', ->
45 |
46 | describe 'findOne', ->
47 |
48 | it 'returns first object without argument', ->
49 | result = testTasks.findOne().fetch()
50 | expect(result).toEqual(jasmine.any(Array))
51 | expect(result?.length).toBe(1)
52 | expect(result[0]).toEqual(jasmine.any(Object))
53 | expect(result[0].text).toEqual('testing1')
54 |
55 | it 'returns object with id as argument', ->
56 | result = testTasks.findOne('3').fetch()
57 | expect(result).toEqual(jasmine.any(Array))
58 | expect(result?.length).toBe(1)
59 | expect(result[0]).toEqual(jasmine.any(Object))
60 | expect(result[0].text).toEqual('testing3')
61 |
62 | describe 'where', ->
63 |
64 | it 'works with basic where', ->
65 | string_where = testTasks.select().where('text = ?', 'testing1').fetch()
66 | expect(string_where?.length).toBe(6)
67 | _.each string_where, (row) -> expect(row?.text).toBe('testing1')
68 |
69 | array_where = testTasks.select().where('text = ?', ['testing1']).fetch()
70 | expect(JSON.stringify(array_where)).toEqual(JSON.stringify(string_where))
71 |
72 | it 'works with basic where and limit', ->
73 | result = testTasks.select().where('text = ?', 'testing1').limit(3).fetch()
74 | expect(result.length).toBe(3)
75 | _.each result, (row) -> expect(row?.text).toBe('testing1')
76 |
77 | it 'works with array where', ->
78 | result = testTasks.select().where('text = ?', ['testing1', 'testing2']).fetch()
79 | expect(result.length).toBe(7)
80 | expect(result[1].text).toBe('testing2')
81 |
82 | it 'works with multiple placeholder', ->
83 | result = testTasks.select().where('id = ? AND text = ?', '2', 'testing2').fetch()
84 | expect(result.length).toBe(1)
85 |
86 | it 'works with multiple placeholders and array wheres', ->
87 | result = testTasks.select().where('id = ? AND text = ?', ['1', '2', '3'], ['testing1', 'testing2']).fetch()
88 | expect(result.length).toBe(2)
89 | expect(result[0].id).toBe('1')
90 | expect(result[1].id).toBe('2')
91 | expect(result[0].text).toBe('testing1')
92 | expect(result[1].text).toBe('testing2')
93 |
94 | describe 'order', ->
95 |
96 | it 'orders correct and ASC by default', ->
97 | asc_default = testTasks.select().order('text').fetch()
98 | asc = testTasks.select().order('text ASC').fetch()
99 | desc = testTasks.select().order('text DESC').fetch()
100 | expect(JSON.stringify(asc_default)).toEqual(JSON.stringify(asc))
101 | expect(JSON.stringify(asc_default)).not.toEqual(JSON.stringify(desc))
102 | expect(JSON.stringify(asc[6])).toEqual(JSON.stringify(desc[1]))
103 |
104 | describe 'first', ->
105 |
106 | it 'picks the right `first`', ->
107 | first = testTasks.select().where('text = ?', 'testing1').order('id DESC').limit(3).first().fetch()
108 | second = testTasks.select().first(2).fetch()
109 | expect(JSON.stringify(first[0])).toEqual(JSON.stringify(second[0]))
110 |
111 | describe 'last', ->
112 |
113 | it 'picks the right `last`', ->
114 | first = testTasks.select().last(4).fetch()
115 | expect(first[1].id).toEqual('7')
116 | second = testTasks.select().where('text = ?', 'testing1').order('id DESC').limit(3).last().fetch()
117 | expect(JSON.stringify(first[0])).toEqual(JSON.stringify(second[0]))
118 |
119 | describe 'take', ->
120 |
121 | it 'picks the right with `take`', ->
122 | first = testTasks.select().order('id DESC').limit(3).take().fetch()
123 | second = testTasks.select().take().fetch()
124 | expect(JSON.stringify(first)).toEqual(JSON.stringify(second))
125 |
126 | describe 'save', ->
127 |
128 | describe 'insert', ->
129 |
130 | it 'creates a string ID if none provided on INSERT', ->
131 | testTasks.insert({ text: 'stringIdTest' }).save()
132 | result = testTasks.select().where('text = ?', 'stringIdTest').fetch()
133 | expect(result[0].id).toEqual(jasmine.any(String))
134 | expect(result[0].id.split('').length).toBeGreaterThan(7)
135 |
136 | describe 'update', ->
137 |
138 | it 'updates correctly with single argument', ->
139 | before = testTasks.select().where('text = ?', 'testing1').fetch()
140 | testTasks.update({ text: 'testing1' }).where('id = ?', '2').save()
141 | after = testTasks.select().where('text = ?', 'testing1').fetch()
142 | expect(before.length + 1).toEqual(after.length)
143 |
144 | it 'calls the server save method', ->
145 | spyOn(Meteor, 'call')
146 | testTasks.update({ text: 'testing1' }).where('id = ?', '2').save()
147 | expect(Meteor.call).toHaveBeenCalled()
148 | expect(Meteor.call.calls.argsFor(0)[0]).toBe('test_tasks_save')
149 |
150 |
151 | it 'updates correctly with multiple arguments', ->
152 | testUsers.update({username: 'PaulOS', age: 100}).where('username = ?', 'paulo').save()
153 | result = testUsers.select().where('username = ?', 'PaulOS').fetch()
154 | expect(result.length).toBe(3)
155 | _.each result, (item) ->
156 | expect(item?.username).toEqual('PaulOS')
157 | expect(item?.age).toBe(100)
158 |
159 | it 'updates not when where does not find entries', ->
160 | testUsers.update({username: 'PaulOS', age: 100}).where('username = ?', 'notexist').save()
161 | result = testUsers.select().where('username = ?', 'PaulOS').fetch()
162 | expect(result.length).toBe(3)
163 |
164 | it 'updates all', ->
165 | first = testTasks.select().where('text = ?', 'testing1').fetch()
166 | testTasks.update( {text: 'testing3'} ).save()
167 | second = testTasks.select().where('text = ?', 'testing3').fetch()
168 | expect(first.length).toBe(7)
169 | expect(second.length).toBe(9)
170 |
171 | describe 'remove', ->
172 |
173 | it 'removes correctly', ->
174 | testTasks.where('id = ?', '2').remove().save()
175 | result = testTasks.select().fetch()
176 | expect(result.length).toBe(8)
177 |
178 | it 'removes all', ->
179 | testTasks.remove().save()
180 | result = testTasks.select().fetch()
181 | expect(result.length).toBe(0)
182 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/lib/collection.coffee:
--------------------------------------------------------------------------------
1 | buffer = []
2 |
3 | ###*
4 | # @summary Namespace for SQL-related items
5 | # @namespace
6 | ###
7 |
8 | SQL.Collection = (connection) ->
9 | unless @ instanceof SQL.Collection
10 | throw new Error 'Use new to construct a SQL.Collection'
11 |
12 | @unvalidated = false
13 | @reactiveData = new (Tracker.Dependency)
14 | @tableName = connection
15 | @table = connection
16 | @saveMethod = @tableName + '_save'
17 | @fetchMethod = @tableName + '_fetch'
18 | @_events = []
19 |
20 | unless @tableName
21 | throw new Error 'First argument to new SQL.Collection must exist'
22 |
23 | unless _.isNull(@tableName) or _.isString(@tableName)
24 | throw new Error 'First argument to new SQL.Collection must be a string or null'
25 |
26 | SQL.Client(@) if Meteor.isClient
27 | SQL.Server(@) if Meteor.isServer
28 |
29 | if Meteor.isClient
30 | # Added will only be triggered on the initial population of the database client side.
31 | # Data added to any client while the page is already loaded will trigger a 'changed event'
32 | @addEventListener 'added', (index, msg, name) ->
33 | @remove().save 'client'
34 | @insert(message).save 'client' for message in msg.results
35 | # Triggering Meteor's reactive data to allow for full stack reactivity
36 | return
37 |
38 | # Changed will be triggered whenever the server database changed while the client has the page open.
39 | # This could happen from an addition, an update, or a removal, from that specific client, or another client
40 | @addEventListener 'changed', (index, msg, name) ->
41 | if msg.removed
42 | @remove().where('id = ?', msg.tableId).save 'client'
43 | else if msg.modified
44 | @update(msg.results).where('id = ?', msg.results.id).save 'client'
45 | else
46 | # The message is a new insertion of a message
47 | # If the message was submitted by this client then the insert message triggered
48 | # by the server should be an update rather than an insert
49 | # We use the unvalidated boolean variabe to keep track of this
50 | if @unvalidated
51 | @update(msg.results).where('id = ?', -1).save 'client'
52 | @unvalidated = false
53 | else
54 | # The data was added by another client so just a regular insert
55 | @insert(msg.results).save 'client'
56 | return
57 |
58 | # setting up the connection between server and client
59 | selfConnection = undefined
60 | subscribeArgs = undefined
61 | if _.isString connection
62 | subscribeArgs = Array::slice.call arguments, 0
63 | name = connection
64 | connection = Meteor.connection if Meteor.isClient
65 | connection = DDP.connect Meteor.absoluteUrl() if Meteor.isServer
66 | else
67 | # SQL.Collection arguments does not use the first argument (the connection)
68 | subscribeArgs = Array::slice.call arguments, 1
69 |
70 | subsBefore = _.keys connection._subscriptions
71 | _.extend @, connection.subscribe.apply(connection, subscribeArgs)
72 | subsNew = _.difference(_.keys(connection._subscriptions), subsBefore)
73 |
74 | unless subsNew.length is 1
75 | throw new Error 'Subscription failed!'
76 |
77 | @subscriptionId = subsNew[0]
78 | buffer.push
79 | connection: connection
80 | name: name
81 | subscriptionId: @subscriptionId
82 | instance: @
83 |
84 | # If first store for this subscription name, register it!
85 | if _.filter(buffer, ((sub) -> sub.name is name and sub.connection is connection)).length is 1
86 | registerStore connection, name
87 | return
88 |
89 | #The code below is originally from Numtel's meteor-mysql but adapted for the purposes of this project (https://github.com/numtel/meteor-mysql/blob/8d7ce8458892f6b255618d884fcde0ec4d04039b/lib/MysqlSubscription.js)
90 |
91 | registerStore = (connection, name) ->
92 | connection.registerStore name,
93 | beginUpdate: (batchSize, reset) ->
94 | update: (msg) ->
95 | idSplit = msg.id.split(':')
96 | sub = _.filter(buffer, (sub) -> sub.subscriptionId == idSplit[0] )[0].instance
97 | if idSplit.length is 1 and msg.msg is 'added' and msg.fields and msg.fields.reset is true
98 | # This message indicates a reset of a result set
99 | sub.dispatchEvent 'reset', msg
100 | sub.splice 0, sub.length
101 | else
102 | index = msg.id
103 | oldRow = undefined
104 | sub.dispatchEvent 'update', index, msg
105 | switch msg.msg
106 | when 'added'
107 | sub.splice index, 0, msg.fields
108 | sub.dispatchEvent msg.msg, index, msg.fields, msg.collection
109 | when 'changed'
110 | sub.splice index, 0, msg.fields
111 | sub.dispatchEvent msg.msg, index, msg.fields, msg.collection
112 | sub.changed()
113 | return
114 | endUpdate: ->
115 | saveOriginals: ->
116 | retrieveOriginals: ->
117 | return
118 |
119 | # Inherit from Array and Tracker.Dependency
120 | SQL.Collection:: = new Array
121 | _.extend(SQL.Collection::, Tracker.Dependency::)
122 | _.extend(SQL.Collection::, SQL.Client::) if Meteor.isClient
123 | _.extend(SQL.Collection::, SQL.Server::) if Meteor.isServer
124 |
125 | SQL.Collection::publish = (collname, pubFunc) ->
126 | methodObj = {}
127 | context = @
128 |
129 | methodObj[@saveMethod] = (input, dataArray) ->
130 | context.save input, dataArray, (error, result) ->
131 | if error
132 | console.error error.message, input
133 |
134 | methodObj[@fetchMethod] = (input, dataArray) ->
135 | context.fetch input, dataArray, (error, result) ->
136 | if error
137 | console.error error.message, input
138 |
139 | Meteor.methods methodObj
140 | Meteor.publish collname, ->
141 | # For this implementation to work you must call getCursor and provide a callback with the select
142 | # statement that needs to be reactive. The 'caboose' on the chain of calls must be autoSelect
143 | # and it must be passed the param 'sub' which is defining in the anon function.
144 | # This is a limitation of our implementation and will be fixed in later versions
145 | { _publishCursor: (sub) ->
146 | pubFunc().autoSelect sub
147 | }
148 | return
149 |
150 | SQL.Collection::_eventRoot = (eventName) ->
151 | eventName.split('.')[0]
152 |
153 | SQL.Collection::_selectEvents = (eventName, invert) ->
154 | eventRoot = undefined
155 | testKey = undefined
156 | testVal = undefined
157 | unless eventName instanceof RegExp
158 | eventRoot = @_eventRoot(eventName)
159 | if eventName is eventRoot
160 | testKey = 'root'
161 | testVal = eventRoot
162 | else
163 | testKey = 'name'
164 | testVal = eventName
165 | _.filter @_events, (event) ->
166 | pass = undefined
167 | if eventName instanceof RegExp
168 | pass = event.name.match(eventName)
169 | else
170 | pass = event[testKey] is testVal
171 | if invert then !pass else pass
172 |
173 | SQL.Collection::addEventListener = (eventName, listener) ->
174 | unless _.isFunction listener
175 | throw new Error 'invalid-listener'
176 |
177 | @_events.push
178 | name: eventName
179 | root: @_eventRoot eventName
180 | listener: listener
181 |
182 | SQL.Collection::initialValue = (eventName, listener) ->
183 | Postgres.select @tableName
184 |
185 | # @param {string} eventName - Remove events of this name, pass without suffix
186 | # to remove all events matching root.
187 |
188 | SQL.Collection::removeEventListener = (eventName) ->
189 | @_events = @_selectEvents eventName, true
190 |
191 | SQL.Collection::dispatchEvent = (eventName) ->
192 | listenerArgs = Array::slice.call arguments, 1
193 | listeners = @_selectEvents eventName
194 | # Newest to oldest
195 | i = listeners.length - 1
196 | while i >= 0
197 | # Return false to stop further handling
198 | if listeners[i].listener.apply(@, listenerArgs) is false
199 | return false
200 | i--
201 | true
202 |
203 | SQL.Collection::reactive = ->
204 | @depend()
205 | @
206 |
207 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/lib/sql.coffee:
--------------------------------------------------------------------------------
1 | SQL.Sql = ->
2 |
3 |
4 | ###*
5 | # Data Types
6 | # @type {{$number: string, $string: string, $json: string, $datetime: string, $float: string, $seq: string, $bool: string}}
7 | # @private
8 | ###
9 |
10 | SQL.Sql::_DataTypes =
11 | $number: 'integer'
12 | $string: 'varchar(255)'
13 | $json: 'json'
14 | $datetime: 'date'
15 | $float: 'decimal'
16 | $seq: 'serial'
17 | $bool: 'boolean'
18 | $timestamp: 'timestamp'
19 |
20 | ###*
21 | # Table Constraints
22 | # @type {{$unique: string, $check: string, $exclude: string, $notnull: string, $default: string, $primary: string}}
23 | # @private
24 | ###
25 |
26 | SQL.Sql::_TableConstraints =
27 | $unique: 'unique'
28 | $check: 'check '
29 | $exclude: 'exclude'
30 | $notnull: 'not null'
31 | $default: 'default '
32 | $primary: 'primary key'
33 |
34 | ###*
35 | # Notes: Deletes cascade
36 | # SQL: DROP TABLE
37 | ###
38 |
39 | SQL.Sql::dropTable = ->
40 | @inputString = "DROP TABLE IF EXISTS #{@table} CASCADE; DROP FUNCTION IF EXISTS notify_trigger_#{@table}() CASCADE;"
41 | @prevFunc = 'DROP TABLE'
42 | @
43 |
44 | ###*
45 | # SQL: INSERT INTO () VALUES ()
46 | # Type: Query
47 | # @param insertObj
48 | ###
49 |
50 | SQL.Sql::insert = (insertObj) ->
51 | valueString = ') VALUES ('
52 | insertObj.id ||= Random.id(19)
53 |
54 | keys = Object.keys insertObj
55 | insertString = "INSERT INTO #{@table} ("
56 | @dataArray = []
57 |
58 | # iterate through array arguments to populate input string parts
59 | for i in [0..keys.length-1]
60 | insertString += "#{keys[i]}, "
61 | @dataArray.push insertObj[keys[i]]
62 | valueString += "$#{i+1}, " if Meteor.isServer
63 | valueString += '?, ' if Meteor.isClient
64 |
65 | @inputString = "#{insertString.substring(0, insertString.length - 2)}#{valueString.substring(0, valueString.length - 2)});"
66 | @prevFunc = 'INSERT'
67 | @
68 |
69 | ###*
70 | # SQL: UPDATE SET () = ()
71 | # Type: Statement Starter
72 | # @param {object} updatesObj
73 | # @param {string} updatesObj Key (Field)
74 | # @param {string} updatesObj Value (Data)
75 | ###
76 |
77 | SQL.Sql::update = (updatesObj) ->
78 | updateField = ''
79 | keys = Object.keys updatesObj
80 |
81 | for i in [0..keys.length-1]
82 | updateField += "#{keys[i]} = #{if _.isString(updatesObj[keys[i]]) then "'#{updatesObj[keys[i]]}'" else updatesObj[keys[i]] }, "
83 |
84 |
85 | @updateString = "UPDATE #{@table} SET #{updateField[0..-3]}"
86 | @prevFunc = 'UPDATE'
87 | @
88 |
89 | ###*
90 | # SQL: DELETE FROM table
91 | # Type: Statement Starter
92 | # Notes: If not chained with where it will remove all rows
93 | ###
94 |
95 | SQL.Sql::remove = ->
96 | @deleteString = "DELETE FROM #{@table}"
97 | @prevFunc = 'DELETE'
98 | @
99 |
100 | ###*
101 | # Parameters: fields (arguments, optional)
102 | # SQL: SELECT fields FROM table, SELECT * FROM table
103 | # Special: May pass table, distinct, field to obtain a single record per unique value
104 | # STATEMENT STARTER/SELECT STRING
105 | #
106 | # SQL: SELECT fields FROM table, SELECT * FROM table
107 | # Type: Statement Starter
108 | # Notes: May pass distinct, field (two separate arguments) to obtain a single record per unique value
109 | # @param {string} [arguments]
110 | # fields to select
111 | ###
112 |
113 | SQL.Sql::select = ->
114 | args = ''
115 | if arguments.length >= 1
116 | for i in [0..arguments.length-1]
117 | args += 'DISTINCT ' if arguments[i] is 'distinct'
118 | args += "#{arguments[i]}, " unless arguments[i] is 'distinct'
119 | args = args.substring(0, args.length - 2)
120 | else
121 | args += '*'
122 |
123 | @selectString = "SELECT #{args} FROM #{@table} "
124 | @prevFunc = 'SELECT'
125 | @
126 |
127 | ###*
128 | # SQL: SELECT * FROM table WHERE table.id = id LIMIT 1; SELECT * FROM table LIMIT 1;
129 | # Notes: If no id is passed will return random
130 | # Type: Query
131 | # @param {number} [id]
132 | ###
133 |
134 | SQL.Sql::findOne = ->
135 | if arguments.length is 1
136 | @inputString = "SELECT * FROM #{@table} WHERE #{@table}.id = $1 LIMIT 1;" if Meteor.isServer
137 | @inputString = "SELECT * FROM #{@table} WHERE #{@table}.id = ? LIMIT 1;" if Meteor.isClient
138 | @dataArray.push arguments[0]
139 | else
140 | @inputString = "SELECT * FROM #{@table} LIMIT 1;"
141 |
142 | @prevFunc = 'FIND ONE'
143 | @
144 |
145 | ###*
146 | # SQL: JOIN joinTable ON field = field
147 | # Type: Statement
148 | # Notes: Parameters can also be all arrays
149 | # @param {String} joinType
150 | # @param {String} fields
151 | # @param {String} joinTable
152 | ###
153 |
154 | SQL.Sql::join = (joinType, fields, joinTable) ->
155 | if _.isArray(joinType)
156 | for i in [0..fields.length-1]
157 | @joinString = " #{joinType[i]} #{joinTable[i][0]} ON #{@table}.#{fields[i]} = #{joinTable[i][0]}.#{joinTable[i][1]}"
158 | else
159 | @joinString = " #{joinType} #{joinTable} ON #{@table}.#{fields} = #{joinTable}.#{joinTable}"
160 |
161 | @prevFunc = 'JOIN'
162 | @
163 |
164 | ###*
165 | # SQL: WHERE field operator comparator, WHERE field1 operator1 comparator1 AND/OR field2 operator2 comparator2, WHERE field IN (x, y)
166 | # Type: Statement
167 | # Notes:
168 | # @param {string} directions
169 | # condition with ?'s for values
170 | # @param {string} values
171 | # values to be used
172 | ###
173 |
174 | SQL.Sql::where = ->
175 | @dataArray = []
176 | where = ''
177 | redux = undefined
178 | substring1 = undefined
179 | substring2 = undefined
180 | where += arguments[0]
181 | for i in [1..arguments.length-1]
182 | redux = where.indexOf '?'
183 | substring1 = where.substring 0, redux
184 | substring2 = where.substring redux + 1, where.length
185 |
186 | if _.isArray(arguments[i])
187 | throw new Error('Invalid input: array is empty') if arguments[i].length is 0
188 | if Meteor.isServer
189 | where = "#{substring1}ANY($#{i})#{substring2}"
190 | @dataArray.push arguments[i]
191 | if Meteor.isClient
192 | where = "#{substring1}ANY(#{_.map(arguments[i], (value) -> if _.isNumber(value) then "#{value}, " else "'#{value}', ").join('')[0..-3]})#{substring2}"
193 | else
194 | where = "#{substring1}$#{i}#{substring2}" if Meteor.isServer
195 | where = "#{substring1}?#{substring2}" if Meteor.isClient
196 | @dataArray.push arguments[i]
197 |
198 | @whereString = " WHERE #{where}"
199 | @
200 |
201 |
202 | ###*
203 | # SQL: ORDER BY fields
204 | # Notes: ASC is default, add DESC after the field name to reverse
205 | # Type: Caboose
206 | # @param {string} fields
207 | ###
208 |
209 | SQL.Sql::order = ->
210 | args = ''
211 | if arguments.length > 1
212 | for i in [0..arguments.length-1]
213 | args += "#{arguments[i]}, "
214 | args = args.substring 0, args.length - 2
215 | else
216 | args = arguments[0]
217 |
218 | @orderString = " ORDER BY #{args}"
219 | @
220 |
221 | ###*
222 | # SQL: LIMIT number
223 | # Type: Caboose
224 | # @param {number} limit
225 | ###
226 |
227 | SQL.Sql::limit = (limit) ->
228 | @limitString = " LIMIT #{limit}"
229 | @
230 |
231 |
232 | ###*
233 | # SQL: OFFSET number
234 | # Type: Caboose
235 | # @param {number} offset
236 | ###
237 |
238 | SQL.Sql::offset = (offset) ->
239 | @offsetString = " OFFSET #{offset}"
240 | @
241 |
242 | ###*
243 | # SQL: GROUP BY field
244 | # Type: Caboose
245 | # @param {string} group
246 | ###
247 |
248 | SQL.Sql::group = (group) ->
249 | @groupString = "GROUP BY #{group}"
250 | @
251 |
252 |
253 | ###*
254 | # SQL: SELECT * FROM table ORDER BY table.id ASC LIMIT 1, SELECT * FROM table ORDER BY table.id ASC LIMIT limit
255 | # Type: Query
256 | # @param limit
257 | ###
258 |
259 | SQL.Sql::first = (limit) ->
260 | limit = limit or 1
261 | @clearAll()
262 | @inputString += "SELECT * FROM #{@table} ORDER BY #{@table}.id ASC LIMIT #{limit};"
263 | @prevFunc = 'FIRST'
264 | @
265 |
266 | ###*
267 | # SQL: SELECT * FROM table ORDER BY table.id DESC LIMIT 1, SELECT * FROM table ORDER BY table.id DESC LIMIT limit
268 | # Type: Query
269 | # @param {number} limit
270 | ###
271 |
272 | SQL.Sql::last = (limit) ->
273 | limit = limit or 1
274 | @clearAll()
275 | @inputString += "SELECT * FROM #{@table} ORDER BY #{@table}.id DESC LIMIT #{limit};"
276 | @prevFunc = 'LAST'
277 | @
278 |
279 | ###*
280 | # SQL: SELECT * FROM table LIMIT 1, SELECT * FROM table LIMIT limit
281 | # Type: Query
282 | # @param {number} limit
283 | # Defaults to 1
284 | ###
285 |
286 | SQL.Sql::take = (limit) ->
287 | limit = limit or 1
288 | @clearAll()
289 | @inputString += "SELECT * FROM #{@table} LIMIT #{limit};"
290 | @prevFunc = 'TAKE'
291 | @
292 |
293 | ###*
294 | # Type: Maintenance
295 | ###
296 |
297 | SQL.Sql::clearAll = ->
298 | @inputString = ''
299 | @autoSelectData = ''
300 | @autoSelectInput = ''
301 | # statement starters
302 | @selectString = ''
303 | @updateString = ''
304 | @deleteString = ''
305 | # chaining statements
306 | @joinString = ''
307 | @whereString = ''
308 | # caboose statements
309 | @orderString = ''
310 | @limitString = ''
311 | @offsetString = ''
312 | @groupString = ''
313 | @havingString = ''
314 | @dataArray = []
315 | # error logging
316 | @prevFunc = ''
317 | return
318 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/tests/jasmine/server/serverSpec.coffee:
--------------------------------------------------------------------------------
1 | describe 'SQL.Server', ->
2 |
3 | tableTestTasks =
4 | text: ['$string', '$notnull']
5 |
6 | tableTestUsers =
7 | username: ['$string', '$notnull']
8 | age: ['$number']
9 |
10 | sqlStub = (name) ->
11 | stub = SQL.Server()
12 | stub.table = name
13 | stub
14 |
15 | testTasks = sqlStub 'test_tasks'
16 | testUsers = sqlStub 'test_users'
17 |
18 | beforeEach (done) ->
19 | try
20 | testTasks.dropTable().save()
21 | testUsers.dropTable().save()
22 | catch e
23 | testTasks.createTable(tableTestTasks)
24 | _(3).times (n) -> testTasks.insert({ id: "#{n+1}", text: "testing#{n + 1}" }).save()
25 | _(5).times (n) -> testTasks.insert({ id: "#{n+1+3}", text: "testing1" }).save()
26 |
27 |
28 | testUsers.createTable(tableTestUsers)
29 | _(3).times (n) ->
30 | testUsers.insert({ id: "#{n*2+1}", username: "eddie#{n + 1}", age: 2 * n }).save()
31 | testUsers.insert({ id: "#{n*2+2}", username: "paulo", age: 27 }).save()
32 | done()
33 |
34 | describe 'exceptions', ->
35 |
36 | it 'throws no error if an existing table gets created again', ->
37 | expect( -> testTasks.createTable(tableTestTasks)).not.toThrow()
38 |
39 | it 'throws no error if an unknown table gets removed', ->
40 | expect( -> testTasks.dropTable('unknownTable').save()).not.toThrow()
41 |
42 | it 'throws no error if an id gets specifically specified on table creation', ->
43 | expect( -> sqlStub('test_table').createTable({id: ['$string', '$primary'], text: ['$string', '$notnull']})).not.toThrow()
44 |
45 | describe 'fetch', ->
46 |
47 | describe 'findOne', ->
48 |
49 | it 'returns first object without argument', ->
50 | result = testTasks.findOne().fetch()?.rows
51 | expect(result).toEqual(jasmine.any(Array))
52 | expect(result.length).toBe(1)
53 | expect(result[0]).toEqual(jasmine.any(Object))
54 | expect(result[0].text).toEqual('testing1')
55 |
56 | it 'returns object with id as argument', ->
57 | result = testTasks.findOne(3).fetch()?.rows
58 | expect(result).toEqual(jasmine.any(Array))
59 | expect(result.length).toBe(1)
60 | expect(result[0]).toEqual(jasmine.any(Object))
61 | expect(result[0].text).toEqual('testing3')
62 |
63 | describe 'where', ->
64 |
65 | it 'works with basic where', ->
66 | string_where = testTasks.select().where('text = ?', 'testing1').fetch()?.rows
67 | expect(string_where?.length).toBe(6)
68 | _.each string_where, (row) -> expect(row?.text).toBe('testing1')
69 |
70 | array_where = testTasks.select().where('text = ?', ['testing1']).fetch()?.rows
71 | expect(JSON.stringify(array_where)).toEqual(JSON.stringify(string_where))
72 |
73 | it 'works with basic where and limit', ->
74 | result = testTasks.select().where('text = ?', 'testing1').limit(3).fetch()?.rows
75 | expect(result.length).toBe(3)
76 | _.each result, (row) -> expect(row?.text).toBe('testing1')
77 |
78 | it 'works with basic where and limit and offset', ->
79 | result = testTasks.select().where('text = ?', 'testing1').limit(3).offset(2).fetch()?.rows
80 | expect(result.length).toBe(3)
81 | _.each result, (row) -> expect(row?.text).toBe('testing1')
82 |
83 | it 'works with basic where and offset', ->
84 | result = testTasks.select().where('text = ?', 'testing1').offset(2).fetch()?.rows
85 | expect(result.length).toBe(4)
86 | _.each result, (row) -> expect(row?.text).toBe('testing1')
87 |
88 | result = testTasks.select().where('text = ?', 'testing1').offset(6).fetch()?.rows
89 | expect(result.length).toBe(0)
90 |
91 | result = testTasks.select().where('text = ?', 'testing1').offset(8).fetch()?.rows
92 | expect(result.length).toBe(0)
93 |
94 | it 'works with array where', ->
95 | result = testTasks.select().where('text = ?', ['testing1', 'testing2']).fetch()?.rows
96 | expect(result.length).toBe(7)
97 | expect(result[1].text).toBe('testing2')
98 |
99 | it 'works with multiple placeholder', ->
100 | result = testTasks.select().where('id = ? AND text = ?', 2, 'testing2').fetch()?.rows
101 | expect(result.length).toBe(1)
102 |
103 | it 'works with multiple placeholders and array wheres', ->
104 | result = testTasks.select().where('id = ? AND text = ?', [1, 2, 3], ['testing1', 'testing2']).fetch()?.rows
105 | expect(result.length).toBe(2)
106 | expect(result[0].id).toBe('1')
107 | expect(result[1].id).toBe('2')
108 | expect(result[0].text).toBe('testing1')
109 | expect(result[1].text).toBe('testing2')
110 |
111 | describe 'order', ->
112 |
113 | it 'orders correct and ASC by default', ->
114 | asc_default = testTasks.select().order('text').fetch()?.rows
115 | asc = testTasks.select().order('text ASC').fetch()?.rows
116 | desc = testTasks.select().order('text DESC').fetch()?.rows
117 | expect(JSON.stringify(asc_default)).toEqual(JSON.stringify(asc))
118 | expect(JSON.stringify(asc_default)).not.toEqual(JSON.stringify(desc))
119 | expect(JSON.stringify(asc[6])).toEqual(JSON.stringify(desc[1]))
120 |
121 | it 'orders correct on chains', ->
122 | first = testTasks.select().where('text = ?', 'testing1').order('id DESC').offset(2).limit(3).fetch()?.rows
123 | second = testTasks.select().where('text = ?', 'testing1').offset(2).order('id DESC').limit(3).fetch()?.rows
124 | expect(JSON.stringify(first)).toEqual(JSON.stringify(second))
125 |
126 | describe 'first', ->
127 |
128 | it 'picks the right `first`', ->
129 | first = testTasks.select().offset(2).where('text = ?', 'testing1').order('id DESC').limit(3).first().fetch()?.rows
130 | second = testTasks.select().first(2).fetch()?.rows
131 | expect(JSON.stringify(first[0])).toEqual(JSON.stringify(second[0]))
132 |
133 | describe 'last', ->
134 |
135 | it 'picks the right `last`', ->
136 | first = testTasks.select().last(4).fetch()?.rows
137 | expect(first[1].id).toEqual('7')
138 | second = testTasks.select().offset(2).where('text = ?', 'testing1').order('id DESC').limit(3).last().fetch()?.rows
139 | expect(JSON.stringify(first[0])).toEqual(JSON.stringify(second[0]))
140 |
141 | describe 'take', ->
142 |
143 | it 'picks the right with `take`', ->
144 | first = testTasks.select().offset(2).order('id DESC').limit(3).take().fetch()?.rows
145 | second = testTasks.select().take().fetch()?.rows
146 | expect(JSON.stringify(first)).toEqual(JSON.stringify(second))
147 |
148 | describe 'save', ->
149 |
150 | it 'makes an synchronous call when no arguments are specified', ->
151 | result = testTasks.update({ text: 'testing1' }).where('text = ?', 'testing1').save()
152 | expect(result).toEqual(jasmine.any(Object))
153 | expect(result.command).toBe('UPDATE')
154 |
155 | it 'makes an asynchronous call when a callback is given', (done) ->
156 | testTasks.update({ text: 'testing1' }).where('text = ?', 'testing1').save Meteor.bindEnvironment (error, result) ->
157 | expect(error).toBe(null)
158 | expect(result).toEqual(jasmine.any(Object))
159 | expect(result.command).toBe('UPDATE')
160 | done()
161 |
162 | it 'makes an asynchronous call with input and data when all three arguments are given', (done) ->
163 | testTasks.save 'UPDATE test_tasks SET (text) = (\'testing1\') WHERE text = $1', ['testing1'], Meteor.bindEnvironment (error, result) ->
164 | expect(error).toBe(null)
165 | expect(result).toEqual(jasmine.any(Object))
166 | expect(result.command).toBe('UPDATE')
167 | done()
168 |
169 | describe 'update', ->
170 |
171 | it 'updates correctly with single argument', ->
172 | before = testTasks.select().where('text = ?', 'testing1').fetch()?.rows
173 | testTasks.update({ text: 'testing1' }).where('text = ?', 'testing2').save()
174 | after = testTasks.select().where('text = ?', 'testing1').fetch()?.rows
175 | expect(before.length + 1).toEqual(after.length)
176 |
177 | it 'updates correctly with multiple arguments', ->
178 | testUsers.update({username: 'PaulOS', age: 100}).where('username = ?', 'paulo').save()
179 | result = testUsers.select().where('username = ?', 'PaulOS').fetch()?.rows
180 | expect(result.length).toBe(3)
181 | _.each result, (item) ->
182 | expect(item?.username).toEqual('PaulOS')
183 | expect(item?.age).toBe(100)
184 |
185 | it 'updates not when where does not find entries', ->
186 | testUsers.update({username: 'PaulOS', age: 100}).where('username = ?', 'notexist').save()
187 | result = testUsers.select().where('username = ?', 'PaulOS').fetch()?.rows
188 | expect(result.length).toBe(0)
189 |
190 | it 'updates all', ->
191 | first = testTasks.select().where('text = ?', 'testing1').fetch()?.rows
192 | testTasks.update( {text: 'testing3'} ).save()
193 | second = testTasks.select().where('text = ?', 'testing3').fetch()?.rows
194 | expect(first.length).toBe(6)
195 | expect(second.length).toBe(8)
196 |
197 | describe 'remove', ->
198 |
199 | it 'removes correctly', ->
200 | testTasks.where('id = ?', '2').remove().save()
201 | result = testTasks.select().fetch()?.rows
202 | expect(result.length).toBe(7)
203 |
204 | it 'removes all', ->
205 | testTasks.remove().save()
206 | result = testTasks.select().fetch()?.rows
207 | expect(result.length).toBe(0)
208 |
--------------------------------------------------------------------------------
/packages/meteor-postgres/lib/server.coffee:
--------------------------------------------------------------------------------
1 | pg = Npm.require('pg')
2 | clientHolder = {}
3 |
4 | removeListeningConnections = ->
5 | for key of clientHolder
6 | clientHolder[key].end()
7 | return
8 |
9 | process.on 'exit', removeListeningConnections
10 |
11 | _.each ['SIGINT', 'SIGHUP', 'SIGTERM'], (sig) ->
12 | process.once sig, ->
13 | removeListeningConnections()
14 | process.kill process.pid, sig
15 |
16 | ###*
17 | # @param Collection
18 | # @constructor
19 | ###
20 |
21 | SQL.Server = (Collection) ->
22 | Collection = Collection or Object.create SQL.Server::
23 | Collection.table = Collection.tableName
24 | Collection.conString = process.env.MP_POSTGRES or process.env.DATABASE_URL
25 |
26 | SQL.Server::clearAll()
27 | Collection
28 |
29 |
30 | # Load all the shared SQL methods
31 | _.extend SQL.Server::, SQL.Sql::
32 |
33 | ###*
34 | # SQL: CREATE TABLE field data_type constraint
35 | # Notes: Required for all SQL Collections, must use prescribed data types and table constraints
36 | # Type: Query
37 | # @param tableObj
38 | ###
39 |
40 | SQL.Server::createTable = (tableObj) ->
41 | startString = "CREATE TABLE IF NOT EXISTS \"#{@table}\" ("
42 | item = undefined
43 | subKey = undefined
44 | valOperator = undefined
45 | inputString = ''
46 |
47 | for key of tableObj
48 | inputString += " #{key} "
49 | inputString += @_DataTypes[tableObj[key][0]]
50 | if _.isArray(tableObj[key]) && tableObj[key].length > 1
51 | for i in [1..(tableObj[key].length-1)]
52 | item = tableObj[key][i]
53 | if _.isObject(item)
54 | subKey = Object.keys item
55 | valOperator = @_TableConstraints[subKey]
56 | inputString += " #{valOperator}#{item[subKey]}"
57 | else
58 | inputString += " #{@_TableConstraints[item]}"
59 | inputString += ', '
60 |
61 | startString += 'id varchar(255) primary key,' if inputString.indexOf(' id') is -1
62 |
63 | watchTrigger = 'watched_table_trigger'
64 | @inputString = """
65 | #{startString}#{inputString} created_at TIMESTAMP default now());
66 |
67 | CREATE OR REPLACE FUNCTION notify_trigger_#{@table}() RETURNS trigger AS $$
68 | BEGIN
69 | IF (TG_OP = 'DELETE') THEN
70 | PERFORM pg_notify('notify_trigger_#{@table}', '[{"' || TG_TABLE_NAME || '":"' || OLD.id || '"}, { "operation": "' || TG_OP || '"}]');
71 | RETURN old;
72 | ELSIF (TG_OP = 'INSERT') THEN
73 | PERFORM pg_notify('notify_trigger_#{@table}', '[{"' || TG_TABLE_NAME || '":"' || NEW.id || '"}, { "operation": "' || TG_OP || '"}]');
74 | RETURN new;
75 | ELSIF (TG_OP = 'UPDATE') THEN
76 | PERFORM pg_notify('notify_trigger_#{@table}', '[{"' || TG_TABLE_NAME || '":"' || NEW.id || '"}, { "operation": "' || TG_OP || '"}]');
77 | RETURN new;
78 | END IF;
79 | END;
80 | $$ LANGUAGE plpgsql;
81 | """
82 |
83 |
84 | @prevFunc = 'CREATE TABLE'
85 |
86 | executeQuery = Meteor.wrapAsync(@exec, @)
87 | executeQuery @inputString, []
88 | executeQuery "DROP TRIGGER IF EXISTS #{watchTrigger} ON #{@table};", []
89 | executeQuery "CREATE TRIGGER #{watchTrigger} AFTER INSERT OR DELETE OR UPDATE ON #{@table} FOR EACH ROW EXECUTE PROCEDURE notify_trigger_#{@table}();", []
90 |
91 | @clearAll()
92 | return
93 |
94 | ###*
95 | # Make a synchronous or asynchronous select on the table.
96 | #
97 | # This makes an synchronous call and returns the result. Otherwise throws and
98 | # error
99 | # query.fetch()
100 | #
101 | # This makes an asynchronous call and runs the callback after the query has
102 | # executed and returns (error, result)
103 | # query.fetch(function(error, result) { ... })
104 | #
105 | # This makes an asynchronous call but uses the provided input and data for the
106 | # query
107 | # query.fetch('SOME QUERY', [data as array], function(error, result) { ... })
108 | #
109 | # Type: Data method
110 | # @param {string} input
111 | # @param {array} data
112 | # @param {function} cb
113 | ###
114 |
115 | SQL.Server::fetch = ->
116 | callback = _.last arguments
117 | input = if arguments.length >= 3 then arguments[0] else undefined
118 | data = if arguments.length >= 3 then arguments[1] else undefined
119 |
120 | data = @dataArray unless data
121 | unless input
122 | starter = @updateString or @deleteString or @selectString
123 | input = if @inputString.length > 0 then @inputString else starter + @joinString + @whereString + @orderString + @limitString + @offsetString + @groupString + @havingString + ';'
124 |
125 | if arguments.length is 0
126 | executeQuery = Meteor.wrapAsync(@exec, @)
127 | result = executeQuery(input, data, callback)
128 | return result
129 | else
130 | @exec input, data, callback
131 | return
132 |
133 | SQL.Server::pg = pg
134 |
135 | SQL.Server::exec = (input, data, cb) ->
136 | pg.connect @conString, (err, client, done) ->
137 | if err and cb
138 | cb err, null
139 | console.log(err) if err
140 |
141 | client.query input, data, (error, results) ->
142 | done()
143 | if cb
144 | cb error, results
145 | @clearAll()
146 |
147 | ###*
148 | # Make a synchronous or asynchronous insert/update/delete on the table.
149 | #
150 | # This makes an synchronous call and returns the result. Otherwise throws and
151 | # error
152 | # query.save()
153 | #
154 | # This makes an asynchronous call and runs the callback after the query has
155 | # executed and returns (error, result)
156 | # query.save(function(error, result) { ... })
157 | #
158 | # This makes an asynchronous call but uses the provided input and data for the
159 | # query
160 | # query.save('SOME QUERY', [data as array], function(error, result) { ... })
161 | #
162 | # Type: Data method
163 | # @param {string} input
164 | # @param {array} data
165 | # @param {function} cb
166 | ###
167 |
168 | SQL.Server::save = ->
169 | callback = _.last arguments
170 | input = if arguments.length >= 3 then arguments[0] else undefined
171 | data = if arguments.length >= 3 then arguments[1] else undefined
172 |
173 | data = @dataArray unless data
174 | unless input
175 | starter = @updateString or @deleteString or @selectString
176 | input = if @inputString.length > 0 then @inputString else starter + @joinString + @whereString + ';'
177 |
178 | if arguments.length is 0
179 | executeQuery = Meteor.wrapAsync(@exec, @)
180 | try
181 | result = executeQuery(input, data, callback)
182 | return result
183 | catch e
184 | console.error e.message
185 | return e
186 | else
187 | @exec input, data, callback
188 | return
189 |
190 | ###*
191 | #
192 | # @param sub
193 | ###
194 |
195 | SQL.Server::autoSelect = (sub) ->
196 | # We need a dedicated client to watch for changes on each table. We store these clients in
197 | # our clientHolder and only create a new one if one does not already exist
198 | self = @
199 | table = @table
200 | strings = {}
201 | strings.select = strings.select or @selectString
202 | strings.join = strings.join or @joinString
203 | strings.prevFunc = @prevFunc
204 |
205 | @autoSelectInput = if @autoSelectInput != '' then @autoSelectInput else @selectString + @joinString + @whereString + @orderString + @limitString + ';'
206 |
207 | @autoSelectData = if @autoSelectData != '' then @autoSelectData else @dataArray
208 | value = @autoSelectInput
209 | @clearAll()
210 |
211 | loadAutoSelectClient = (name, cb) ->
212 | # Function to load a new client, store it, and then send it to the function to add the watcher
213 | client = new pg.Client(process.env.MP_POSTGRES)
214 | client.on 'notification', (msg) -> self._notificationsDDP(sub, strings, msg)
215 | client.connect()
216 | clientHolder[name] = client
217 | cb client
218 |
219 | autoSelectHelper = (client) ->
220 | # Selecting all from the table
221 | client.query value, (error, results) ->
222 | if error
223 | console.error "#{error.message} in autoSelect top"
224 | else
225 | sub._session.send
226 | msg: 'added'
227 | collection: sub._name
228 | id: sub._subscriptionId
229 | fields:
230 | reset: false
231 | results: results.rows
232 |
233 | # Adding notification triggers
234 | query = client.query "LISTEN notify_trigger_#{table}"
235 | client.on 'notification', (msg) -> self._notificationsDDP(sub, strings, msg)
236 |
237 | # Checking to see if this table already has a dedicated client before adding the listener
238 | if clientHolder[table]
239 | autoSelectHelper clientHolder[table]
240 | else
241 | loadAutoSelectClient table, autoSelectHelper
242 | return
243 |
244 | SQL.Server::_notificationsDDP = (sub, strings, msg) ->
245 | message = JSON.parse msg.payload
246 | k = sub._name
247 | if message[1].operation is 'DELETE'
248 | tableId = message[0][k]
249 | sub._session.send
250 | msg: 'changed'
251 | collection: sub._name
252 | id: sub._subscriptionId
253 | index: tableId
254 | fields:
255 | removed: true
256 | reset: false
257 | tableId: tableId
258 |
259 | else if message[1].operation is 'UPDATE'
260 | selectString = "#{strings.select + strings.join} WHERE #{@table}.id = '#{message[0][@table]}'"
261 | pg.connect process.env.MP_POSTGRES, (err, clientSub, done) ->
262 | if err
263 | console.log(err, "in #{prevFunc} #{@table}")
264 |
265 | clientSub.query selectString, @autoSelectData, (error, results) ->
266 | if error
267 | console.error error.message, selectString
268 | else
269 | done()
270 | sub._session.send
271 | msg: 'changed'
272 | collection: sub._name
273 | id: sub._subscriptionId
274 | index: tableId
275 | fields:
276 | modified: true
277 | removed: false
278 | reset: false
279 | results: results.rows[0]
280 |
281 | else if message[1].operation is 'INSERT'
282 | selectString = "#{strings.select + strings.join} WHERE #{@table}.id = '#{message[0][@table]}'"
283 | pg.connect process.env.MP_POSTGRES, (err, clientSub, done) ->
284 | if err
285 | console.log(err, "in #{prevFunc} #{@table}")
286 |
287 | clientSub.query selectString, @autoSelectData, (error, results) ->
288 | if error
289 | console.error error.message, selectString
290 | else
291 | done()
292 | ddpPayload =
293 | msg: 'changed'
294 | collection: sub._name
295 | id: sub._subscriptionId
296 | fields:
297 | removed: false
298 | reset: false
299 | results: results.rows[0]
300 | sub._session.send ddpPayload
301 |
--------------------------------------------------------------------------------