entangled 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ea4626c7d1bb040d403f4da6b8b64021c81a29f
4
- data.tar.gz: 639932af1572cfd2006295af882d23519e0c1f00
3
+ metadata.gz: da1eee63cbd14ff868400bd98f77d164b7625df0
4
+ data.tar.gz: 97a8f2215ebd469f203ad179d61f8beb9872aa63
5
5
  SHA512:
6
- metadata.gz: 034e96b2cb4f1748c4ef748448974e1b54fd2fcaa7db7ff469eae55d45365ec5cee0da26daaacae459b2d903bc698b1e610547bfd56d52e61e2ba904acfdf196
7
- data.tar.gz: 55d59ceeeddb3277e3d971db4926b10270e0e591062ce738b4dc2b9f7db8fc39bba89cf56b257bde97ff0542f342e59f353d793b192e5ff4f5a8bd11b45028ed
6
+ metadata.gz: 69972bd90800fdbd7bf64c96a27b25eca95b633971116351c604d0049ec164a9c5d4696d5a9278f990ee7b5fccf47ff804d2f325aefd88272f2798cd3e863c58
7
+ data.tar.gz: 8cf42b1b3bf5f5a6bbc1009d87c4e657c429cd7392ea1acb7895f411fbb422c05f04de7e1998332358c99594b8bcd2b4f4d60e2c759f6f3b11a2af864ea7c69b
data/README.md CHANGED
@@ -268,6 +268,13 @@ Pick if you want to use Entangled with plain JavaScript or with Angular:
268
268
  - [entangled-js](https://github.com/dchacke/entangled-js)
269
269
  - [entangled-angular](https://github.com/dchacke/entangled-angular)
270
270
 
271
+ The following versions are compatible:
272
+
273
+ | entangled.gem | entangled-js.js | entangled-angular.js |
274
+ |---------------|-----------------|----------------------|
275
+ | 1.4.1 | 1.3.1 | 1.3.1 |
276
+ | 1.4.1 | 1.3.0 | 1.3.0 |
277
+
271
278
  ## A Note On Cases
272
279
  The case conventions differ in Ruby and JavaScript. `snake_case` is the standard in Ruby, whereas `camelCase` is the standard in JavaScript.
273
280
 
@@ -281,22 +288,33 @@ This gem is best used for Rails apps that serve as APIs only and are not concern
281
288
  ## Limitations
282
289
  The gem relies heavily on convention over configuration and currently only works with restful style controllers as shown above. More features will be available soon. See the list of development priorities below.
283
290
 
291
+ ## Debugging Websockets
292
+ To debug websockets from your terminal, you can use curl. For example, to do a handshake with a socket at `/messages` (a route you need to have set up), you can do the following:
293
+
294
+ ```shell
295
+ curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: echo.websocket.org" -H "Origin: http://localhost:3000" http://localhost:3000/messages
296
+ ```
297
+
298
+ More information [here](http://www.thenerdary.net/post/24889968081/debugging-websockets-with-curl).
299
+
284
300
  ## Development Priorities
285
301
  The following features are to be implemented next:
286
302
 
287
- - Make broadcast method non-blocking using [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby); update Readme and repo description accordingly
288
- - Add compatibility table for Ruby/Angular/JS versions
303
+ - Throw error if parent id not set on child when trying to fetch parent
304
+ - Support multiple `hasMany` and `belongsTo` associations in front end
305
+ - Put up example application (the todo list?)
289
306
  - Make prefix of create path `create_message` instead of `create_messages`
290
- - Support `belongsTo` in front end
291
307
  - Support `has_one` association in back end and front end and route helper for single resource
292
- - Add offline capabilities
308
+ - Support scoping in back end
309
+ - Add support for scopes and where clauses in front end once back end can do scopes à la [Spyke](https://github.com/balvig/spyke)
310
+ - Display results of interactions to client immediately without going through the server; add server interactions to queue and constantly dequeue; if result from server conflicts with client state, update client accordingly
311
+ - Add offline capabilities, i.e. only dequeue server interactions once internet connection established
293
312
  - Add authentication - with JWT?
294
313
  - On Heroku, tasks are always in different order depending on which ones are checked off and not
295
314
  - Add `$onChange` function to objects - or could a simple $watch and $watchCollection suffice?
296
315
  - Add diagram on how it works to Readme
297
316
  - GNU instead of MIT? Or something else? How to switch?
298
317
  - Contact Jessy to tweet about it!
299
- - Handle errors gracefully (e.g. finding a non-existent id, etc, authorization error in the back end, timeouts, etc)
300
318
  - Test controllers (see https://github.com/ngauthier/tubesock/issues/41)
301
319
  - Add `.destroyAll()` function to `Resources`
302
320
 
@@ -61,165 +61,170 @@ module Entangled
61
61
  def broadcast(&block)
62
62
  # Use hijack to handle sockets
63
63
  hijack do |tubesock|
64
- # Assuming restful controllers, the behavior of
65
- # this method has to change depending on the action
66
- # it's being used in
67
- case action_name
68
-
69
- # If the controller action is 'index', a collection
70
- # of records should be broadcast
71
- when 'index'
72
- yield
73
-
74
- # The following code will run if an instance
75
- # variable with the plural resource name has been
76
- # assigned in yield. For example, if a
77
- # TacosController's index action looked something
78
- # like this:
79
-
80
- # def index
81
- # broadcast do
82
- # @tacos = Taco.all
83
- # end
84
- # end
85
-
86
- # ...then @tacos will be broadcast to all connected
87
- # clients. The variable name, in this example,
88
- # has to be "@tacos"
89
- if collection
90
- redis_thread = Thread.new do
91
- redis.subscribe channel do |on|
92
- # Broadcast messages to all connected clients
93
- on.message do |channel, message|
94
- tubesock.send_data message
64
+ begin
65
+ # Assuming restful controllers, the behavior of
66
+ # this method has to change depending on the action
67
+ # it's being used in
68
+ case action_name
69
+
70
+ # If the controller action is 'index', a collection
71
+ # of records should be broadcast
72
+ when 'index'
73
+ execute_action(tubesock, block)
74
+
75
+ # The following code will run if an instance
76
+ # variable with the plural resource name has been
77
+ # assigned in yield. For example, if a
78
+ # TacosController's index action looked something
79
+ # like this:
80
+
81
+ # def index
82
+ # broadcast do
83
+ # @tacos = Taco.all
84
+ # end
85
+ # end
86
+
87
+ # ...then @tacos will be broadcast to all connected
88
+ # clients. The variable name, in this example,
89
+ # has to be "@tacos"
90
+ if collection
91
+ redis_thread = Thread.new do
92
+ redis.subscribe channel do |on|
93
+ # Broadcast messages to all connected clients
94
+ on.message do |channel, message|
95
+ tubesock.send_data message
96
+ end
97
+
98
+ # Send message to whoever just subscribed
99
+ tubesock.send_data({
100
+ resources: collection
101
+ }.to_json)
102
+
103
+ close_db_connection
95
104
  end
105
+ end
96
106
 
97
- # Send message to whoever just subscribed
98
- tubesock.send_data({
99
- resources: collection
100
- }.to_json)
101
-
102
- close_db_connection
107
+ # When client disconnects, kill the thread
108
+ tubesock.onclose do
109
+ redis_thread.kill
103
110
  end
104
111
  end
105
112
 
106
- # When client disconnects, kill the thread
107
- tubesock.onclose do
108
- redis_thread.kill
109
- end
110
- end
113
+ # If the controller's action name is 'show', a single record
114
+ # should be broadcast
115
+ when 'show'
116
+ execute_action(tubesock, block)
117
+
118
+ # The following code will run if an instance variable
119
+ # with the singular resource name has been assigned in
120
+ # yield. For example, if a TacosController's show action
121
+ # looked something like this:
122
+
123
+ # def show
124
+ # broadcast do
125
+ # @taco = Taco.find(params[:id])
126
+ # end
127
+ # end
111
128
 
112
- # If the controller's action name is 'show', a single record
113
- # should be broadcast
114
- when 'show'
115
- yield
116
-
117
- # The following code will run if an instance variable
118
- # with the singular resource name has been assigned in
119
- # yield. For example, if a TacosController's show action
120
- # looked something like this:
121
-
122
- # def show
123
- # broadcast do
124
- # @taco = Taco.find(params[:id])
125
- # end
126
- # end
127
-
128
- # ...then @taco will be broadcast to all connected clients.
129
- # The variable name, in this example, has to be "@taco"
130
- if member
131
- redis_thread = Thread.new do
132
- redis.subscribe channel do |on|
133
- # Broadcast messages to all connected clients
134
- on.message do |channel, message|
135
- tubesock.send_data message
129
+ # ...then @taco will be broadcast to all connected clients.
130
+ # The variable name, in this example, has to be "@taco"
131
+ if member
132
+ redis_thread = Thread.new do
133
+ redis.subscribe channel do |on|
134
+ # Broadcast messages to all connected clients
135
+ on.message do |channel, message|
136
+ tubesock.send_data message
137
+ end
138
+
139
+ # Send message to whoever just subscribed
140
+ tubesock.send_data({
141
+ resource: member
142
+ }.to_json)
143
+
144
+ close_db_connection
136
145
  end
146
+ end
137
147
 
138
- # Send message to whoever just subscribed
148
+ # When client disconnects, kill the thread
149
+ tubesock.onclose do
150
+ redis_thread.kill
151
+ end
152
+ end
153
+
154
+ # If the controller's action name is 'create', a record should be
155
+ # created. Before yielding, the params hash has to be prepared
156
+ # with attributes sent to the socket. The actual publishing
157
+ # happens in the model's callback
158
+ when 'create'
159
+ tubesock.onmessage do |m|
160
+ set_resource_params(m)
161
+ execute_action(tubesock, block)
162
+
163
+ # Send resource that was just created back to client. The resource
164
+ # on the client will be overridden with this one. This is important
165
+ # so that the id, created_at and updated_at and possibly other
166
+ # attributes arrive on the client
167
+ if member
139
168
  tubesock.send_data({
140
169
  resource: member
141
170
  }.to_json)
142
-
143
- close_db_connection
144
171
  end
145
- end
146
172
 
147
- # When client disconnects, kill the thread
148
- tubesock.onclose do
149
- redis_thread.kill
150
- end
151
- end
152
-
153
- # If the controller's action name is 'create', a record should be
154
- # created. Before yielding, the params hash has to be prepared
155
- # with attributes sent to the socket. The actual publishing
156
- # happens in the model's callback
157
- when 'create'
158
- tubesock.onmessage do |m|
159
- set_resource_params(m)
160
- yield
161
-
162
- # Send resource that was just created back to client. The resource
163
- # on the client will be overridden with this one. This is important
164
- # so that the id, created_at and updated_at and possibly other
165
- # attributes arrive on the client
166
- if member
167
- tubesock.send_data({
168
- resource: member
169
- }.to_json)
173
+ close_db_connection
170
174
  end
171
175
 
172
- close_db_connection
173
- end
176
+ # If the controller's action name is 'update', a record should be
177
+ # updated. Before yielding, the params hash has to be prepared
178
+ # with attributes sent to the socket
179
+ when 'update'
180
+ tubesock.onmessage do |m|
181
+ set_resource_params(m)
182
+ execute_action(tubesock, block)
183
+
184
+ # Send resource that was just updated back to client. The resource
185
+ # on the client will be overridden with this one. This is important
186
+ # so that the new updated_at and possibly other attributes arrive
187
+ # on the client
188
+ if member
189
+ tubesock.send_data({
190
+ resource: member
191
+ }.to_json)
192
+ end
174
193
 
175
- # If the controller's action name is 'update', a record should be
176
- # updated. Before yielding, the params hash has to be prepared
177
- # with attributes sent to the socket
178
- when 'update'
179
- tubesock.onmessage do |m|
180
- set_resource_params(m)
181
- yield
182
-
183
- # Send resource that was just updated back to client. The resource
184
- # on the client will be overridden with this one. This is important
185
- # so that the new updated_at and possibly other attributes arrive
186
- # on the client
187
- if member
188
- tubesock.send_data({
189
- resource: member
190
- }.to_json)
194
+ close_db_connection
191
195
  end
192
196
 
193
- close_db_connection
194
- end
197
+ when 'destroy'
198
+ tubesock.onmessage do |m|
199
+ execute_action(tubesock, block)
195
200
 
196
- when 'destroy'
197
- tubesock.onmessage do |m|
198
- yield
201
+ # Send resource that was just destroyed back to client
202
+ if member
203
+ tubesock.send_data({
204
+ resource: member
205
+ }.to_json)
206
+ end
199
207
 
200
- # Send resource that was just destroyed back to client
201
- if member
202
- tubesock.send_data({
203
- resource: member
204
- }.to_json)
208
+ close_db_connection
205
209
  end
206
210
 
207
- close_db_connection
208
- end
209
-
210
- # For every other controller action, simply wrap whatever is
211
- # yielded in the tubesock block to execute it in the context
212
- # of the socket. Other custom actions can be added through this
213
- else
214
- tubesock.onmessage do |m|
215
- # If message was sent, attach to params (rescue exception if
216
- # message not valid JSON or message not present)
217
- params.merge!(JSON.parse(m)) rescue nil
211
+ # For every other controller action, simply wrap whatever is
212
+ # yielded in the tubesock block to execute it in the context
213
+ # of the socket. Other custom actions can be added through this
214
+ else
215
+ tubesock.onmessage do |m|
216
+ # If message was sent, attach to params (rescue exception if
217
+ # message not valid JSON or message not present)
218
+ params.merge!(JSON.parse(m)) rescue nil
218
219
 
219
- yield
220
+ execute_action(tubesock, block)
220
221
 
221
- close_db_connection
222
+ close_db_connection
223
+ end
222
224
  end
225
+ rescue Exception => e
226
+ Rails.logger.error e
227
+ close_db_connection
223
228
  end
224
229
  end
225
230
  end
@@ -227,6 +232,27 @@ module Entangled
227
232
  def set_resource_params(message_from_socket)
228
233
  params[resource_name.to_sym] = JSON.parse(message_from_socket)
229
234
  end
235
+
236
+ # Run the actual controller action that is passed as
237
+ # a block to the broadcast method and catch any exceptions
238
+ # as needed
239
+ def execute_action(tubesock, block)
240
+ begin
241
+ # Execute block
242
+ block.call
243
+ rescue Exception => e
244
+ # Log the error
245
+ Rails.logger.error e.message
246
+
247
+ # Print stack strace
248
+ puts e.backtrace
249
+
250
+ # Send error message to client
251
+ tubesock.send_data({
252
+ error: e.message
253
+ }.to_json)
254
+ end
255
+ end
230
256
  end
231
257
 
232
258
  def self.included(receiver)
@@ -1,3 +1,3 @@
1
1
  module Entangled
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -3,10 +3,10 @@
3
3
  angular.module('entangledTest')
4
4
 
5
5
  .controller('ListCtrl', function($scope, $routeParams, List) {
6
- List.find($routeParams.id, function(list) {
6
+ List.find($routeParams.id, function(err, list) {
7
7
  $scope.$apply(function() {
8
8
  $scope.list = list;
9
- $scope.list.items().all(function(items) {
9
+ $scope.list.items().all(function(err, items) {
10
10
  $scope.$apply(function() {
11
11
  $scope.items = items;
12
12
  });
@@ -17,7 +17,7 @@ angular.module('entangledTest')
17
17
  list.$destroy();
18
18
  };
19
19
 
20
- List.all(function(lists) {
20
+ List.all(function(err, lists) {
21
21
  $scope.$apply(function() {
22
22
  $scope.lists = lists;
23
23
  });
@@ -7,10 +7,10 @@ angular.module('entangled', [])
7
7
  // Every response coming from the server will be wrapped
8
8
  // in a Resource constructor to represent a CRUD-able
9
9
  // resource that can be saved and destroyed using the
10
- // methods $save(), $destroy, and others. A Resource also
10
+ // methods $save, $destroy, and others. A Resource also
11
11
  // stores the socket's URL it was retrieved from so it
12
12
  // can be reused for other requests.
13
- function Resource(params, webSocketUrl, hasMany) {
13
+ function Resource(params, webSocketUrl, hasMany, belongsTo) {
14
14
  // Assign properties
15
15
  for (var key in params) {
16
16
  // Skip inherent object properties
@@ -20,14 +20,50 @@ angular.module('entangled', [])
20
20
  }
21
21
 
22
22
  // Assign socket URL
23
- this.webSocketUrl = webSocketUrl;
23
+ this.socket = webSocketUrl;
24
+ this.webSocketUrl = function() {
25
+ // If a parent is associated, the websocket URL looks something
26
+ // like ws://localhost:3000/parents/:parentId/children. The
27
+ // following replaces :parentId with the actual parent's id
28
+ if (belongsTo) {
29
+ var parentIdPlaceholder = ':' + belongsTo + 'Id';
30
+ var parentId = this[belongsTo + 'Id'];
31
+ var newWebSocketUrl = this.socket.replace(parentIdPlaceholder, parentId);
32
+ return newWebSocketUrl;
33
+ } else {
34
+ // If no parent is associated, just return the plain URL
35
+ return this.socket;
36
+ }
37
+ };
24
38
 
25
- // Assign has many relationship
39
+ // Assign parent relationship
26
40
  if (hasMany) {
27
41
  this[hasMany] = function() {
28
- return new Entangled(this.webSocketUrl + '/' + this.id + '/' + hasMany);
42
+ return new Entangled(this.webSocketUrl() + '/' + this.id + '/' + hasMany);
29
43
  };
30
44
  }
45
+
46
+ // Assign child relationship
47
+ if (belongsTo) {
48
+ this[belongsTo] = function(callback) {
49
+ // Infer socket URL to parent from child's socket URL
50
+ var socketElements = this.webSocketUrl().split('/');
51
+ var parentSocket = socketElements.slice(socketElements.length - 6, 4).join('/');
52
+
53
+ // Instantiate new parent and get its id
54
+ var Parent = new Entangled(parentSocket);
55
+ var parentId = this[belongsTo + 'Id'];
56
+
57
+ // Find parent and pass it to callback
58
+ Parent.find(parentId, function(err, parent) {
59
+ if (err) {
60
+ callback(err)
61
+ } else {
62
+ callback(null, parent);
63
+ }
64
+ });
65
+ }.bind(this);
66
+ }
31
67
  };
32
68
 
33
69
  // $save() will send a request to the server
@@ -37,7 +73,7 @@ angular.module('entangled', [])
37
73
  Resource.prototype.$save = function(callback) {
38
74
  if (this.id) {
39
75
  // Update
40
- var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/update');
76
+ var socket = new WebSocket(this.webSocketUrl() + '/' + this.id + '/update');
41
77
  socket.onopen = function() {
42
78
  socket.send(this.asSnakeJSON());
43
79
  }.bind(this);
@@ -47,6 +83,14 @@ angular.module('entangled', [])
47
83
  if (event.data) {
48
84
  var data = JSON.parse(event.data);
49
85
 
86
+ if (data.error) {
87
+ if (callback) {
88
+ callback(new Error(data.error));
89
+ }
90
+
91
+ return;
92
+ }
93
+
50
94
  // Assign/override new data (such as updated_at, etc)
51
95
  if (data.resource) {
52
96
  for (key in data.resource) {
@@ -57,17 +101,19 @@ angular.module('entangled', [])
57
101
 
58
102
  // Assign has many association. The association
59
103
  // can only be available to a persisted record
60
- this[this.hasMany] = new Entangled(this.webSocketUrl + '/' + this.id + '/' + this.hasMany);
104
+ this[this.hasMany] = new Entangled(this.webSocketUrl() + '/' + this.id + '/' + this.hasMany);
61
105
 
62
106
  // Pass 'this' to callback for create
63
107
  // function so this the create function
64
108
  // can pass the created resource to its
65
109
  // own callback; not needed for $save per se
66
- if (callback) callback(this);
110
+ if (callback) {
111
+ callback(null, this);
112
+ }
67
113
  }.bind(this);
68
114
  } else {
69
115
  // Create
70
- var socket = new WebSocket(this.webSocketUrl + '/create');
116
+ var socket = new WebSocket(this.webSocketUrl() + '/create');
71
117
 
72
118
  // Send attributes to server
73
119
  socket.onopen = function() {
@@ -79,6 +125,14 @@ angular.module('entangled', [])
79
125
  if (event.data) {
80
126
  var data = JSON.parse(event.data);
81
127
 
128
+ if (data.error) {
129
+ if (callback) {
130
+ callback(new Error(data.error));
131
+ }
132
+
133
+ return;
134
+ }
135
+
82
136
  // Assign/override new data (such as id, created_at,
83
137
  // updated_at, etc)
84
138
  if (data.resource) {
@@ -92,7 +146,9 @@ angular.module('entangled', [])
92
146
  // function so this the create function
93
147
  // can pass the created resource to its
94
148
  // own callback; not needed for $save per se
95
- if (callback) callback(this);
149
+ if (callback) {
150
+ callback(null, this)
151
+ };
96
152
  }.bind(this);
97
153
  }
98
154
  };
@@ -114,7 +170,8 @@ angular.module('entangled', [])
114
170
  // $destroy() will send a request to the server to
115
171
  // destroy an existing record.
116
172
  Resource.prototype.$destroy = function(callback) {
117
- var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/destroy');
173
+ var socket = new WebSocket(this.webSocketUrl() + '/' + this.id + '/destroy');
174
+ // var socket = new WebSocket(this.webSocketUrl() + '/' + '2343345' + '/destroy');
118
175
 
119
176
  socket.onopen = function() {
120
177
  // It's fine to send an empty message since the
@@ -127,6 +184,14 @@ angular.module('entangled', [])
127
184
  if (event.data) {
128
185
  var data = JSON.parse(event.data);
129
186
 
187
+ if (data.error) {
188
+ if (callback) {
189
+ callback(new Error(data.error));
190
+ }
191
+
192
+ return;
193
+ }
194
+
130
195
  // Assign/override new data
131
196
  if (data.resource) {
132
197
  for (key in data.resource) {
@@ -142,7 +207,9 @@ angular.module('entangled', [])
142
207
  Object.freeze(this);
143
208
  }
144
209
 
145
- if (callback) callback(this);
210
+ if (callback) {
211
+ callback(null, this);
212
+ }
146
213
  }.bind(this);
147
214
  };
148
215
 
@@ -223,16 +290,22 @@ angular.module('entangled', [])
223
290
  this.webSocketUrl = webSocketUrl;
224
291
  };
225
292
 
226
- // hasMany() adds the appropriate association and
227
- // sets up websockets accordingly
228
- Entangled.prototype.hasMany = function(resources) {
229
- this.hasMany = resources;
293
+ // hasMany() adds the appropriate parent association
294
+ // and sets up websockets accordingly
295
+ Entangled.prototype.hasMany = function(resourcesName) {
296
+ this.hasMany = resourcesName;
297
+ };
298
+
299
+ // belongsTo() adds the appropriate child association
300
+ // and sets up websockets accordingly
301
+ Entangled.prototype.belongsTo = function(resourceName) {
302
+ this.belongsTo = resourceName;
230
303
  };
231
304
 
232
305
  // Function to instantiate a new Resource, optionally
233
306
  // with given parameters
234
307
  Entangled.prototype.new = function(params) {
235
- return new Resource(params, this.webSocketUrl, this.hasMany);
308
+ return new Resource(params, this.webSocketUrl, this.hasMany, this.belongsTo);
236
309
  };
237
310
 
238
311
  // Retrieve all Resources from the root of the socket's
@@ -246,6 +319,12 @@ angular.module('entangled', [])
246
319
  // Convert message to JSON
247
320
  var data = JSON.parse(event.data);
248
321
 
322
+ if (data.error) {
323
+ callback(new Error(data.error));
324
+
325
+ return;
326
+ }
327
+
249
328
  // If the collection of Resources was sent
250
329
  if (data.resources) {
251
330
  // Store retrieved Resources in property
@@ -288,7 +367,7 @@ angular.module('entangled', [])
288
367
 
289
368
  // Run the callback and pass in the
290
369
  // resulting collection
291
- callback(this.resources.all);
370
+ callback(null, this.resources.all);
292
371
  }.bind(this);
293
372
  };
294
373
 
@@ -309,6 +388,12 @@ angular.module('entangled', [])
309
388
  // Parse message and convert to JSON
310
389
  var data = JSON.parse(event.data);
311
390
 
391
+ if (data.error) {
392
+ callback(new Error(data.error));
393
+
394
+ return;
395
+ }
396
+
312
397
  if (data.resource && !data.action) {
313
398
  // If the Resource was sent from the server,
314
399
  // store it
@@ -330,7 +415,7 @@ angular.module('entangled', [])
330
415
  }
331
416
 
332
417
  // Run callback with retrieved Resource
333
- callback(this.resource);
418
+ callback(null, this.resource);
334
419
  }.bind(this);
335
420
  };
336
421
 
@@ -5,7 +5,8 @@ describe('Entangled', function() {
5
5
 
6
6
  var $injector,
7
7
  Entangled,
8
- List;
8
+ List,
9
+ Item;
9
10
 
10
11
  beforeEach(inject(function() {
11
12
  $injector = angular.injector(['entangled']);
@@ -13,6 +14,9 @@ describe('Entangled', function() {
13
14
 
14
15
  List = new Entangled('ws://localhost:3000/lists');
15
16
  List.hasMany('items');
17
+
18
+ Item = new Entangled('ws://localhost:3000/lists/:listId/items');
19
+ Item.belongsTo('list');
16
20
  }));
17
21
 
18
22
  describe('constructor', function() {
@@ -31,8 +35,13 @@ describe('Entangled', function() {
31
35
 
32
36
  describe('.all', function() {
33
37
  it('fetches all lists', function(done) {
34
- List.all(function(lists) {
38
+ List.all(function(err, lists) {
39
+ // Assert that err is null
40
+ expect(err).toEqual(null);
41
+
42
+ // Assert that lists are set
35
43
  expect(lists).toEqual(jasmine.any(Array));
44
+
36
45
  done();
37
46
  });
38
47
  });
@@ -40,9 +49,14 @@ describe('Entangled', function() {
40
49
 
41
50
  describe('.create', function() {
42
51
  it('creates a list', function(done) {
43
- List.create({ name: 'foo' }, function(list) {
52
+ List.create({ name: 'foo' }, function(err, list) {
53
+ // Assert that err is null
54
+ expect(err).toEqual(null);
55
+
56
+ // Assert that list was created successfully
44
57
  expect(list.id).toBeDefined();
45
58
  expect(list.createdAt).toBeDefined();
59
+
46
60
  done();
47
61
  });
48
62
  });
@@ -50,8 +64,13 @@ describe('Entangled', function() {
50
64
  it('receives validation messages', function(done) {
51
65
  // Leave out name, causing model validations
52
66
  // in ActiveRecord to fail
53
- List.create({}, function(list) {
67
+ List.create({}, function(err, list) {
68
+ // Assert that err is null
69
+ expect(err).toEqual(null);
70
+
71
+ // Assert that validation errors are attached
54
72
  expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
73
+
55
74
  done();
56
75
  });
57
76
  });
@@ -59,14 +78,35 @@ describe('Entangled', function() {
59
78
 
60
79
  describe('.find', function() {
61
80
  it('finds a list', function(done) {
62
- List.create({ name: 'foo' }, function(list) {
63
- List.find(list.id, function(list) {
81
+ List.create({ name: 'foo' }, function(err, list) {
82
+ // Assert that err is null
83
+ expect(err).toEqual(null);
84
+
85
+ List.find(list.id, function(err, list) {
86
+ // Assert that err is null
87
+ expect(err).toEqual(null);
88
+
89
+ // Assert that list was found
64
90
  expect(list.id).toBeDefined();
65
91
  expect(list.createdAt).toBeDefined();
92
+
66
93
  done();
67
94
  });
68
95
  });
69
96
  });
97
+
98
+ it('receives an error when looking for a list that does not exist', function(done) {
99
+ List.find('not an id', function(err, list) {
100
+ // Assert that error is set
101
+ expect(err).toEqual(jasmine.any(Error));
102
+ expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
103
+
104
+ // Assert that no second parameter was passed to callback
105
+ expect(list).not.toBeDefined();
106
+
107
+ done();
108
+ })
109
+ });
70
110
  });
71
111
 
72
112
  describe('#$save', function() {
@@ -74,9 +114,14 @@ describe('Entangled', function() {
74
114
  it('saves a new list', function(done) {
75
115
  var list = List.new({ name: 'foo' });
76
116
 
77
- list.$save(function() {
117
+ list.$save(function(err, list) {
118
+ // Assert that err is null
119
+ expect(err).toEqual(null);
120
+
121
+ // Assert that list was persisted
78
122
  expect(list.id).toBeDefined();
79
123
  expect(list.createdAt).toBeDefined();
124
+
80
125
  done();
81
126
  });
82
127
  });
@@ -86,8 +131,13 @@ describe('Entangled', function() {
86
131
  // in ActiveRecord to fail
87
132
  var list = List.new();
88
133
 
89
- list.$save(function() {
134
+ list.$save(function(err, list) {
135
+ // Assert that err is null
136
+ expect(err).toEqual(null);
137
+
138
+ // Assert that validation errors are attached
90
139
  expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
140
+
91
141
  done();
92
142
  });
93
143
  });
@@ -95,31 +145,72 @@ describe('Entangled', function() {
95
145
 
96
146
  describe('existing record', function() {
97
147
  it('updates an existing list', function(done) {
98
- List.create({ name: 'foo' }, function(list) {
148
+ List.create({ name: 'foo' }, function(err, list) {
149
+ // Assert that err is null
150
+ expect(err).toEqual(null);
151
+
152
+ // Prepare for update
99
153
  list.name = 'new name';
154
+
155
+ // Remember old updatedAt to compare once saved
100
156
  var oldUpdatedAt = list.updatedAt;
101
157
 
102
- list.$save(function() {
158
+ // Save
159
+ list.$save(function(err, list) {
160
+ // Assert that err is null
161
+ expect(err).toEqual(null);
162
+
163
+ // Assert that list was updated successfully
103
164
  expect(list.name).toBe('new name');
104
165
  expect(list.updatedAt).not.toEqual(oldUpdatedAt);
166
+
105
167
  done();
106
168
  });
107
169
  });
108
170
  });
109
171
 
110
172
  it('receives validation messages', function(done) {
111
- List.create({ name: 'foo' }, function(list) {
173
+ List.create({ name: 'foo' }, function(err, list) {
174
+ // Assert that err is null
175
+ expect(err).toEqual(null);
176
+
112
177
  // Make invalid by setting the name to an
113
178
  // empty string, causing model validations
114
179
  // in ActiveRecord to fail
115
180
  list.name = '';
116
181
  var oldUpdatedAt = list.updatedAt;
117
182
 
118
- list.$save(function() {
183
+ list.$save(function(err, list) {
184
+ // Assert that err is null
185
+ expect(err).toEqual(null);
186
+
119
187
  // Assert that the list was not updated
120
188
  // by the server
121
189
  expect(list.updatedAt).toBe(oldUpdatedAt);
122
190
  expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
191
+
192
+ done();
193
+ });
194
+ });
195
+ });
196
+
197
+ it('receives an error when trying to save a record without being able to find it', function(done) {
198
+ List.create({ name: 'foo' }, function(err, list) {
199
+ // Assert that err is null
200
+ expect(err).toEqual(null);
201
+
202
+ // Prepare for update
203
+ list.id = 'not an id';
204
+
205
+ // Save
206
+ list.$save(function(err, list) {
207
+ // Assert that error is set
208
+ expect(err).toEqual(jasmine.any(Error));
209
+ expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
210
+
211
+ // Assert that no second parameter was passed to callback
212
+ expect(list).not.toBeDefined();
213
+
123
214
  done();
124
215
  });
125
216
  });
@@ -129,29 +220,69 @@ describe('Entangled', function() {
129
220
 
130
221
  describe('#$update', function() {
131
222
  it('updates a list in place', function(done) {
132
- List.create({ name: 'foo' }, function(list) {
223
+ List.create({ name: 'foo' }, function(err, list) {
224
+ // Assert that err is null
225
+ expect(err).toEqual(null);
226
+
227
+ // Remember old updatedAt to compare once updated
133
228
  var oldUpdatedAt = list.updatedAt;
134
229
 
135
- list.$update({ name: 'new name' }, function() {
230
+ // Update
231
+ list.$update({ name: 'new name' }, function(err, list) {
232
+ // Assert that err is null
233
+ expect(err).toEqual(null);
234
+
235
+ // Assert that list was updated successfully
136
236
  expect(list.name).toBe('new name');
137
237
  expect(list.updatedAt).not.toEqual(oldUpdatedAt);
238
+
138
239
  done();
139
240
  });
140
241
  });
141
242
  });
142
243
 
143
244
  it('receives validation messages', function(done) {
144
- List.create({ name: 'foo' }, function(list) {
245
+ List.create({ name: 'foo' }, function(err, list) {
246
+ // Assert that err is null
247
+ expect(err).toEqual(null);
248
+
249
+ // Remember old updatedAt to compare once updated
145
250
  var oldUpdatedAt = list.updatedAt;
146
251
 
147
252
  // Make invalid by setting the name to an
148
253
  // empty string, causing model validations
149
254
  // in ActiveRecord to fail
150
- list.$update({ name: '' }, function() {
255
+ list.$update({ name: '' }, function(err, list) {
256
+ // Assert that err is null
257
+ expect(err).toEqual(null);
258
+
151
259
  // Assert that the list was not updated
152
260
  // by the server
153
261
  expect(list.updatedAt).toBe(oldUpdatedAt);
154
262
  expect(list.errors.name.indexOf("can't be blank") > -1).toBeTruthy();
263
+
264
+ done();
265
+ });
266
+ });
267
+ });
268
+
269
+ it('receives an error when trying to save a record without being able to find it', function(done) {
270
+ List.create({ name: 'foo' }, function(err, list) {
271
+ // Assert that err is null
272
+ expect(err).toEqual(null);
273
+
274
+ // Prepare for update
275
+ // list.id = 'not an id';
276
+
277
+ // Save
278
+ list.$update({ id: 'not an id' }, function(err, list) {
279
+ // Assert that error is set
280
+ expect(err).toEqual(jasmine.any(Error));
281
+ expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
282
+
283
+ // Assert that no second parameter was passed to callback
284
+ expect(list).not.toBeDefined();
285
+
155
286
  done();
156
287
  });
157
288
  });
@@ -160,13 +291,25 @@ describe('Entangled', function() {
160
291
 
161
292
  describe('#$destroy', function() {
162
293
  it('destroys a list', function(done) {
163
- List.create({ name: 'foo' }, function(list) {
164
- list.$destroy(function() {
294
+ List.create({ name: 'foo' }, function(err, list) {
295
+ // Assert that err is null
296
+ expect(err).toEqual(null);
297
+
298
+ list.$destroy(function(err, list) {
299
+ // Assert that no error happened
300
+ expect(err).toEqual(null);
301
+
165
302
  // If successfully destroyed, it won't be
166
303
  // included in the collection of all records
167
304
  // anymore
168
- List.all(function(lists) {
305
+ List.all(function(err, lists) {
306
+ // Assert that no error happened
307
+ expect(err).toEqual(null);
308
+
309
+ // Assert that list not included in lists
310
+ // anymore
169
311
  expect(lists.indexOf(list)).toBe(-1);
312
+
170
313
  done();
171
314
  });
172
315
  });
@@ -174,22 +317,59 @@ describe('Entangled', function() {
174
317
  });
175
318
 
176
319
  it('marks the record as destroyed', function(done) {
177
- List.create({ name: 'foo' }, function(list) {
178
- list.$destroy(function() {
320
+ List.create({ name: 'foo' }, function(err, list) {
321
+ // Assert that err is null
322
+ expect(err).toEqual(null);
323
+
324
+ list.$destroy(function(err, list) {
325
+ // Assert that no error happened
326
+ expect(err).toEqual(null);
327
+
328
+ // Asser that list is destroyed
179
329
  expect(list.destroyed).toBeTruthy();
330
+
180
331
  done();
181
332
  });
182
333
  });
183
334
  });
184
335
 
185
336
  it('freezes the record', function(done) {
186
- List.create({ name: 'foo' }, function(list) {
187
- list.$destroy(function() {
337
+ List.create({ name: 'foo' }, function(err, list) {
338
+ // Assert that err is null
339
+ expect(err).toEqual(null);
340
+
341
+ list.$destroy(function(err, list) {
342
+ // Assert that no error happened
343
+ expect(err).toEqual(null);
344
+
345
+ // Assert that list is frozen
188
346
  expect(Object.isFrozen(list)).toBeTruthy();
347
+
189
348
  done();
190
349
  });
191
350
  });
192
351
  });
352
+
353
+ it('has an error when an exception happens in the back end', function(done) {
354
+ // Try to destroy list that's not in the database,
355
+ // thereby triggering an exception in the back end,
356
+ // which should trigger an exception in the front end
357
+ var list = List.new({ id: 1234 });
358
+
359
+ list.$destroy(function(err, noList) {
360
+ // Assert that error is defined and has the right message
361
+ expect(err).toEqual(jasmine.any(Error));
362
+ expect(err.message).toEqual("Couldn't find List with 'id'=1234");
363
+
364
+ // Assert that list was not frozen
365
+ expect(Object.isFrozen(list)).not.toBeTruthy();
366
+
367
+ // Assert that no second parameter was sent to callback
368
+ expect(noList).not.toBeDefined();
369
+
370
+ done();
371
+ });
372
+ });
193
373
  });
194
374
 
195
375
  describe('#$valid', function() {
@@ -322,17 +502,89 @@ describe('Entangled', function() {
322
502
  });
323
503
 
324
504
  describe('Associations', function() {
325
- it('has many items', function(done) {
326
- List.create({ name: 'foo' }, function(list) {
327
- // Assert that relationship defined
328
- expect(list.items).toBeDefined();
329
-
330
- // Assert that relationship is also
331
- // an instance of Entangled, meaning
332
- // in turn that all class and instance
333
- // methods are available on it
334
- expect(list.items().constructor.name).toBe('Entangled');
335
- done();
505
+ describe('List', function() {
506
+ it('has many items', function(done) {
507
+ List.create({ name: 'foo' }, function(err, list) {
508
+ // Assert that err is null
509
+ expect(err).toEqual(null);
510
+
511
+ // Assert that relationship defined
512
+ expect(list.items).toBeDefined();
513
+
514
+ // Assert that relationship is also
515
+ // an instance of Entangled, meaning
516
+ // in turn that all class and instance
517
+ // methods are available on it
518
+ expect(list.items().constructor.name).toBe('Entangled');
519
+
520
+ done();
521
+ });
522
+ });
523
+ });
524
+
525
+ describe('Item', function() {
526
+ it('belongs to a list', function(done) {
527
+ // Create parent list
528
+ List.create({ name: 'foo' }, function(err, list) {
529
+ // Assert that err is null
530
+ expect(err).toEqual(null);
531
+
532
+ // Create child item
533
+ Item.create({ name: 'foo', listId: list.id }, function(err, item) {
534
+ // Assert that err is null
535
+ expect(err).toEqual(null);
536
+
537
+ // Assert that creation successful
538
+ expect(item.$persisted()).toBeTruthy();
539
+
540
+ // Assert parent relationship
541
+ // expect(item.list).toBe(list);
542
+ var originalList = list;
543
+
544
+ item.list(function(err, list) {
545
+ // Assert that err is null
546
+ expect(err).toEqual(null);
547
+
548
+ // Assert that the original list and the parent
549
+ // are the same
550
+ expect(originalList.id).toBe(list.id);
551
+
552
+ done();
553
+ });
554
+ });
555
+ });
556
+ });
557
+
558
+ it('receives an error when trying to find a parent that does not exist', function(done) {
559
+ // Create parent list
560
+ List.create({ name: 'foo' }, function(err, list) {
561
+ // Assert that err is null
562
+ expect(err).toEqual(null);
563
+
564
+ // Create child item
565
+ Item.create({ name: 'foo', listId: list.id }, function(err, item) {
566
+ // Assert that err is null
567
+ expect(err).toEqual(null);
568
+
569
+ // Assert that creation successful
570
+ expect(item.$persisted()).toBeTruthy();
571
+
572
+ // Change parent id to a non-existent id
573
+ item.listId = 'not an id';
574
+
575
+ item.list(function(err, list) {
576
+ // Assert that err is set
577
+ expect(err).toEqual(jasmine.any(Error));
578
+ expect(err.message).toEqual("Couldn't find List with 'id'=not an id");
579
+
580
+ // Assert that no second parameter was passed
581
+ // to the callback
582
+ expect(list).not.toBeDefined();
583
+
584
+ done();
585
+ });
586
+ });
587
+ });
336
588
  });
337
589
  });
338
590
  });
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: entangled
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dennis Charles Hackethal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-13 00:00:00.000000000 Z
11
+ date: 2015-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler