entangled 0.0.20 → 0.0.21

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: 47e523fb330c5b6afaf2a591a5b182cb0f7bb6cd
4
- data.tar.gz: c2f08b77e3c57ba6f7ac7141b52f7bb4c2cfd286
3
+ metadata.gz: a57bb2d361ca6fc01d21e41081913491ec6d3f96
4
+ data.tar.gz: 8e76dffbad36c6d4f5ce3189852c054d919bbe36
5
5
  SHA512:
6
- metadata.gz: c31e87f3e09508d7c56361af3e828f84da4c0156b7ad9763fe3d888dfc2038c5ef3d1df2dbdbf473f6173707f44625d8da37dbaba59ea6e7d69d3fc2992ae85b
7
- data.tar.gz: 102f1d094bd646896aade1752327b3edbe0eaea578b1e904572e6ad206d7e701acd369b74950630fc38308a7fc0ab828c4351b4e3f66e1fcc160849d780982e2
6
+ metadata.gz: 313a25571895c0076d87989f4efae4cda3d8642255fa930844b02306b12f2dca7d6d5d5236ca8778d64588a5f55e8f555fc2c44a9d9155e1276e0f7b57339a99
7
+ data.tar.gz: 0fd5d35de6ee95bdb9f77b1a21df7959b17e19048dcd19e6bea3794ebb9e1980cf4c22ff589a51320fca923c08bf01d55a471531083b51a0f60a59a8f7a2d0c1
data/README.md CHANGED
@@ -6,7 +6,7 @@ Real time is important. Users have come to expect real time behavior from every
6
6
 
7
7
  Entangled stores and syncs data from ActiveRecord instantly across every device. It is a layer behind your models and controllers that pushes updates to all connected clients in real time. It is cross-browser compatible and offers real time validations.
8
8
 
9
- Currently, Entangled runs on Rails 4.2 in the back end and Angular in the front end.
9
+ Currently, Entangled runs on Rails 4.2 and Ruby 2.0 in the back end, and Angular 1.3 in the front end.
10
10
 
11
11
  ## Installation
12
12
  Add this line to your application's Gemfile:
@@ -299,6 +299,101 @@ $scope.message.$save(function() {
299
299
 
300
300
  Note that `$valid()` and `$invalid()` should only be used after $saving a resource, i.e. in the callback of `$save`, since they don't actually invoke server side validations. They only check if a resource contains errors.
301
301
 
302
+ #### Associations
303
+ What if you want to only fetch and subscribe to children that belong to a specific parent? Or maybe you want to create a child in your front end and assign it to a specific parent?
304
+
305
+ Entangled currently supports one `belongs_to` association per model.
306
+
307
+ For example, imagine the following Parent > Children relationship in your models:
308
+
309
+ ```ruby
310
+ class Parent < ActiveRecord::Base
311
+ include Entangled::Model
312
+ entangle
313
+
314
+ has_many :children
315
+ end
316
+
317
+ class Child < ActiveRecord::Base
318
+ include Entangled::Model
319
+ entangle
320
+
321
+ belongs_to :parent
322
+ end
323
+ ```
324
+
325
+ To reflect this in your front end, you just need to add three things to your app:
326
+
327
+ - Nest your routes so that they resemble the parent/child relationship:
328
+
329
+ ```ruby
330
+ sockets_for :parents do
331
+ sockets_for :children
332
+ end
333
+ ```
334
+
335
+ - Adjust the `index` and `create` actions in your `ChildrenController` so that they look like this:
336
+
337
+ ```ruby
338
+ class ChildrenController < ApplicationController
339
+ include Entangled::Controller
340
+
341
+ # Fetch children of specific parent
342
+ def index
343
+ broadcast do
344
+ @children = Parent.find(params[:parent_id]).children
345
+ end
346
+ end
347
+
348
+ # Create child of specific parent
349
+ def create
350
+ broadcast do
351
+ @child = Parent.find(params[:parent_id]).children.create(child_params)
352
+ end
353
+ end
354
+
355
+ # show, update and destroy don't need to be nested
356
+ end
357
+ ```
358
+
359
+ - Lastly, inform your Angular parent service about the association:
360
+
361
+ ```javascript
362
+ app.factory('Parent', function(Entangled) {
363
+ // Instantiate Entangled service
364
+ var entangled = new Entangled('ws://localhost:3000/parents');
365
+
366
+ // Set up association
367
+ entangled.hasMany('children');
368
+
369
+ return entangled;
370
+ });
371
+ ```
372
+
373
+ This makes a `children()` function available on your parent records on which you can chain all other functions to fetch/manipulate data:
374
+
375
+ ```javascript
376
+ Parent.find(1, function(parent) {
377
+ parent.children().all(function(children) {
378
+ // children here all belong to parent with id 1
379
+ });
380
+
381
+ parent.children().find(1, function(child) {
382
+ // child has id 1 and belongs to parent with id 1
383
+ });
384
+
385
+ parent.children().create({ foo: 'bar' }, function(child) {
386
+ // child has been persisted and associated with parent
387
+ });
388
+
389
+ // etc
390
+ });
391
+ ```
392
+
393
+ This is the way to go if you want to fetch records that only belong to a certain record, or create records that should belong to a parent record. In short, it is ideal to scope records to parent records.
394
+
395
+ Naturally, all nested records are also synced in real time across all connected clients.
396
+
302
397
  #### Persistence
303
398
  Just as with ActiveRecord's `persisted?` method, you can use `$persisted()` on an object to check if it was successfully stored in the database.
304
399
 
@@ -319,7 +414,6 @@ The following features are to be implemented next:
319
414
  - Offline capabilities - when client is disconnected, put websocket interactions in a queue and dequeue all once connected again
320
415
  - Support for authentication
321
416
  - Support for associations
322
- - Remove angular dependencies from bower package (they're currently all being downloaded as well when doing bower install)
323
417
  - On Heroku (maybe in production in general), objects are always in different order depending on their attributes
324
418
  - Add $onChange listener to objects
325
419
  - Add diagram on how it works to Readme
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency 'bundler', '~> 1.7'
22
22
  s.add_development_dependency 'rake', '~> 10.0'
23
23
  s.add_development_dependency 'rspec-rails', '~> 3.2'
24
+ s.add_development_dependency 'shoulda-matchers', '~> 2.6'
24
25
  s.add_development_dependency 'sqlite3', '~> 1.3'
25
26
  s.add_development_dependency 'byebug', '~> 3.5'
26
27
  s.add_development_dependency 'bourne', '~> 1.6'
@@ -1 +1 @@
1
- angular.module("entangled",[]).factory("Entangled",function(){var e=function(e,r){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);this.webSocketUrl=r};e.prototype.$save=function(e){var r=this;if(this.id){var t=new WebSocket(r.webSocketUrl+"/"+r.id+"/update");t.onopen=function(){t.send(JSON.stringify(r))},t.onmessage=function(t){if(t.data){var o=JSON.parse(t.data);if(o.resource)for(key in o.resource)r[key]=o.resource[key]}e&&e(r)}}else{var t=new WebSocket(r.webSocketUrl+"/create");t.onopen=function(){t.send(JSON.stringify(r))},t.onmessage=function(t){if(t.data){var o=JSON.parse(t.data);if(o.resource)for(key in o.resource)r[key]=o.resource[key]}e&&e(r)}}},e.prototype.$update=function(e,r){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);this.$save(r)},e.prototype.$destroy=function(e){var r=new WebSocket(this.webSocketUrl+"/"+this.id+"/destroy");r.onopen=function(){r.send(null),e&&e()}},e.prototype.$valid=function(){return!(this.errors&&Object.keys(this.errors).length)},e.prototype.$invalid=function(){return!this.$valid()},e.prototype.$persisted=function(){return!!this.id};var r=function(r,t){this.all=[];for(var o=0;o<r.length;o++){var s=new e(r[o],t);this.all.push(s)}},t=function(e){this.webSocketUrl=e};return t.prototype["new"]=function(r){return new e(r,this.webSocketUrl)},t.prototype.all=function(t){var o=new WebSocket(this.webSocketUrl);o.onmessage=function(s){if(s.data.length){var n=JSON.parse(s.data);if(n.resources)this.resources=new r(n.resources,o.url);else if(n.action)if("create"===n.action)this.resources.all.push(new e(n.resource,o.url));else if("update"===n.action){for(var i,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===n.resource.id&&(i=a);this.resources.all[i]=new e(n.resource,o.url)}else if("destroy"===n.action){for(var i,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===n.resource.id&&(i=a);this.resources.all.splice(i,1)}else console.log("Something else other than CRUD happened..."),console.log(n)}t(this.resources.all)}},t.prototype.create=function(e,r){var t=this["new"](e);t.$save(r)},t.prototype.find=function(r,t){var o=this.webSocketUrl,s=new WebSocket(o+"/"+r);s.onmessage=function(r){if(r.data.length){var s=JSON.parse(r.data);s.resource&&!s.action?this.resource=new e(s.resource,o):s.action?"update"===s.action?this.resource=new e(s.resource,o):"destroy"===s.action&&(this.resource=void 0):(console.log("Something else other than CRUD happened..."),console.log(s))}t(this.resource)}},t});
1
+ angular.module("entangled",[]).factory("Entangled",function(){var e=function(e,t,r){for(var o in e)e.hasOwnProperty(o)&&(this[o]=e[o]);this.webSocketUrl=t,r&&(this[r]=function(){return new s(this.webSocketUrl+"/"+this.id+"/"+r)})};e.prototype.$save=function(e){if(this.id){var t=new WebSocket(this.webSocketUrl+"/"+this.id+"/update");t.onopen=function(){t.send(JSON.stringify(this))}.bind(this),t.onmessage=function(t){if(t.data){var r=JSON.parse(t.data);if(r.resource)for(key in r.resource)this[key]=r.resource[key]}this[this.hasMany]=new s(this.webSocketUrl+"/"+this.id+"/"+this.hasMany),e&&e(this)}.bind(this)}else{var t=new WebSocket(this.webSocketUrl+"/create");t.onopen=function(){t.send(JSON.stringify(this))}.bind(this),t.onmessage=function(t){if(t.data){var s=JSON.parse(t.data);if(s.resource)for(key in s.resource)this[key]=s.resource[key]}e&&e(this)}.bind(this)}},e.prototype.$update=function(e,t){for(var s in e)e.hasOwnProperty(s)&&(this[s]=e[s]);this.$save(t)},e.prototype.$destroy=function(e){var t=new WebSocket(this.webSocketUrl+"/"+this.id+"/destroy");t.onopen=function(){t.send(null),e&&e()}},e.prototype.$valid=function(){return!(this.errors&&Object.keys(this.errors).length)},e.prototype.$invalid=function(){return!this.$valid()},e.prototype.$persisted=function(){return!!this.id};var t=function(t,s,r){this.all=[];for(var o=0;o<t.length;o++){var i=new e(t[o],s,r);this.all.push(i)}},s=function(e){this.webSocketUrl=e};return s.prototype.hasMany=function(e){this.hasMany=e},s.prototype["new"]=function(t){return new e(t,this.webSocketUrl,this.hasMany)},s.prototype.all=function(s){var r=new WebSocket(this.webSocketUrl);r.onmessage=function(o){if(o.data.length){var i=JSON.parse(o.data);if(i.resources)this.resources=new t(i.resources,r.url,this.hasMany);else if(i.action)if("create"===i.action)this.resources.all.push(new e(i.resource,r.url,this.hasMany));else if("update"===i.action){for(var n,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===i.resource.id&&(n=a);this.resources.all[n]=new e(i.resource,r.url,this.hasMany)}else if("destroy"===i.action){for(var n,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===i.resource.id&&(n=a);this.resources.all.splice(n,1)}else console.log("Something else other than CRUD happened..."),console.log(i)}s(this.resources.all)}.bind(this)},s.prototype.create=function(e,t){var s=this["new"](e);s.$save(t)},s.prototype.find=function(t,s){var r=this.webSocketUrl,o=new WebSocket(r+"/"+t);o.onmessage=function(t){if(t.data.length){var o=JSON.parse(t.data);o.resource&&!o.action?this.resource=new e(o.resource,r,this.hasMany):o.action?"update"===o.action?this.resource=new e(o.resource,r,this.hasMany):"destroy"===o.action&&(this.resource=void 0):(console.log("Something else other than CRUD happened..."),console.log(o))}s(this.resource)}.bind(this)},s});
@@ -42,15 +42,9 @@ module Entangled
42
42
  instance_variable_get(:"@#{resource_name}")
43
43
  end
44
44
 
45
- # Channel name for single resource, used in show action
46
- def member_channel
47
- member.channel
48
- end
49
-
50
- # Channel name for collection of resources, used in index
51
- # action
52
- def collection_channel
53
- model.channel
45
+ # Infer channel from current path
46
+ def channel
47
+ request.path
54
48
  end
55
49
 
56
50
  # Close the connection to the DB so as to
@@ -94,7 +88,7 @@ module Entangled
94
88
  # has to be "@tacos"
95
89
  if collection
96
90
  redis_thread = Thread.new do
97
- redis.subscribe collection_channel do |on|
91
+ redis.subscribe channel do |on|
98
92
  # Broadcast messages to all connected clients
99
93
  on.message do |channel, message|
100
94
  tubesock.send_data message
@@ -135,7 +129,7 @@ module Entangled
135
129
  # The variable name, in this example, has to be "@taco"
136
130
  if member
137
131
  redis_thread = Thread.new do
138
- redis.subscribe member_channel do |on|
132
+ redis.subscribe channel do |on|
139
133
  # Broadcast messages to all connected clients
140
134
  on.message do |channel, message|
141
135
  tubesock.send_data message
@@ -55,12 +55,6 @@ module Entangled
55
55
  [:create, :update, :destroy]
56
56
  end
57
57
 
58
- # The inferred channel name. For example, if the class name
59
- # is DeliciousTaco, the inferred channel name is "delicious_tacos"
60
- def channel
61
- name.underscore.pluralize
62
- end
63
-
64
58
  # Creates callbacks in the extented model
65
59
  def create_hook(name)
66
60
  send :"after_#{name}", -> { publish(name) }
@@ -79,13 +73,40 @@ module Entangled
79
73
  attributes.merge(errors: errors).as_json
80
74
  end
81
75
 
82
- # The inferred channel name for a single record
83
- # containing the inferred channel name from the class
84
- # and the record's id. For example, if it's a
85
- # DeliciousTaco with the id 1, the inferred channel
86
- # name for the single record is "delicious_tacos/1"
87
- def channel
88
- "#{self.class.channel}/#{self.to_param}"
76
+ # The channel name for a single record containing the
77
+ # inferred channel name from the class and the record's
78
+ # id. For example, if it's a DeliciousTaco with the id 1,
79
+ # the member channel for the single record is "delicious_tacos/1".
80
+ # Nesting is automatically applied through the use of
81
+ # the collection channel.
82
+ #
83
+ # The member channel has to be the same as the path to
84
+ # the resource's show action, including a leading
85
+ # forward slash
86
+ def member_channel
87
+ "#{collection_channel}/#{self.to_param}"
88
+ end
89
+
90
+ # The inferred channel name for the collection. For example,
91
+ # if the class name is DeliciousTaco, the collection channel
92
+ # is "delicious_tacos".
93
+ #
94
+ # If the model belongs to another model, the channel is nested
95
+ # accordingly. For example, if a child belongs to a parent,
96
+ # the child's collection channel is "parents/1/children".
97
+ #
98
+ # The collection channel has to be the same as the path to
99
+ # the resource's index action, including a leading forward slash
100
+ def collection_channel
101
+ belongs_to_assocations = self.class.reflect_on_all_associations(:belongs_to)
102
+ own_channel = self.class.name.underscore.pluralize
103
+
104
+ if belongs_to_assocations.any?
105
+ parent = send(belongs_to_assocations.first.name)
106
+ "#{parent.member_channel}/#{own_channel}"
107
+ else
108
+ "/#{own_channel}"
109
+ end
89
110
  end
90
111
 
91
112
  private
@@ -96,13 +117,13 @@ module Entangled
96
117
  def publish(action)
97
118
  # Publish to model's channel
98
119
  redis.publish(
99
- self.class.channel,
120
+ collection_channel,
100
121
  json(action)
101
122
  )
102
123
 
103
124
  # Publish to record#s channel
104
125
  redis.publish(
105
- channel,
126
+ member_channel,
106
127
  json(action)
107
128
  )
108
129
  end
@@ -1,3 +1,3 @@
1
1
  module Entangled
2
- VERSION = "0.0.20"
2
+ VERSION = "0.0.21"
3
3
  end
@@ -0,0 +1,39 @@
1
+ class ItemsController < ApplicationController
2
+ include Entangled::Controller
3
+
4
+ def index
5
+ broadcast do
6
+ @items = List.find(params[:list_id]).items
7
+ end
8
+ end
9
+
10
+ def create
11
+ broadcast do
12
+ @item = List.find(params[:list_id]).items.create(item_params)
13
+ end
14
+ end
15
+
16
+ def show
17
+ broadcast do
18
+ @item = Item.find(params[:id])
19
+ end
20
+ end
21
+
22
+ def update
23
+ broadcast do
24
+ @item = Item.find(params[:id])
25
+ @item.update(item_params)
26
+ end
27
+ end
28
+
29
+ def destroy
30
+ broadcast do
31
+ Item.find(params[:id]).destroy
32
+ end
33
+ end
34
+
35
+ private
36
+ def item_params
37
+ params.require(:item).permit(:name, :complete)
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ class ListsController < ApplicationController
2
+ include Entangled::Controller
3
+
4
+ def index
5
+ broadcast do
6
+ @lists = List.all
7
+ end
8
+ end
9
+
10
+ def create
11
+ broadcast do
12
+ @list = List.create(list_params)
13
+ end
14
+ end
15
+
16
+ def show
17
+ broadcast do
18
+ @list = List.find(params[:id])
19
+ end
20
+ end
21
+
22
+ def destroy
23
+ broadcast do
24
+ List.find(params[:id]).destroy
25
+ end
26
+ end
27
+
28
+ private
29
+ def list_params
30
+ params.require(:list).permit(:name)
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ class Item < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ belongs_to :list, required: true
6
+ end
@@ -0,0 +1,6 @@
1
+ class List < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ has_many :items, dependent: :destroy
6
+ end
@@ -0,0 +1,9 @@
1
+ class CreateLists < ActiveRecord::Migration
2
+ def change
3
+ create_table :lists do |t|
4
+ t.string :name
5
+
6
+ t.timestamps null: false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ class CreateItems < ActiveRecord::Migration
2
+ def change
3
+ create_table :items do |t|
4
+ t.string :name
5
+ t.boolean :complete, default: false
6
+ t.integer :list_id
7
+
8
+ t.timestamps null: false
9
+ end
10
+ end
11
+ end
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20150223004852) do
14
+ ActiveRecord::Schema.define(version: 20150314055847) do
15
15
 
16
16
  create_table "barfoos", force: :cascade do |t|
17
17
  t.text "body"
@@ -37,6 +37,20 @@ ActiveRecord::Schema.define(version: 20150223004852) do
37
37
  t.datetime "updated_at", null: false
38
38
  end
39
39
 
40
+ create_table "items", force: :cascade do |t|
41
+ t.string "name"
42
+ t.boolean "complete", default: false
43
+ t.integer "list_id"
44
+ t.datetime "created_at", null: false
45
+ t.datetime "updated_at", null: false
46
+ end
47
+
48
+ create_table "lists", force: :cascade do |t|
49
+ t.string "name"
50
+ t.datetime "created_at", null: false
51
+ t.datetime "updated_at", null: false
52
+ end
53
+
40
54
  create_table "messages", force: :cascade do |t|
41
55
  t.text "body"
42
56
  t.datetime "created_at", null: false
@@ -10,10 +10,19 @@ angular.module('entangledTest', [
10
10
  .when('/', {
11
11
  templateUrl: 'views/messages/index.html',
12
12
  controller: 'MessagesCtrl'
13
- }).when('/messages/:id', {
13
+ })
14
+ .when('/messages/:id', {
14
15
  templateUrl: 'views/messages/show.html',
15
16
  controller: 'MessageCtrl'
16
17
  })
18
+ .when('/lists', {
19
+ templateUrl: 'views/lists/index.html',
20
+ controller: 'ListsCtrl'
21
+ })
22
+ .when('/lists/:id', {
23
+ templateUrl: 'views/lists/show.html',
24
+ controller: 'ListCtrl'
25
+ })
17
26
  .otherwise({
18
27
  redirectTo: '/'
19
28
  });
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ angular.module('entangledTest')
4
+
5
+ .controller('ListCtrl', function($scope, $routeParams, List) {
6
+ List.find($routeParams.id, function(list) {
7
+ $scope.$apply(function() {
8
+ $scope.list = list;
9
+ $scope.list.items().all(function(items) {
10
+ $scope.$apply(function() {
11
+ $scope.items = items;
12
+ });
13
+ });
14
+ });
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ angular.module('entangledTest')
4
+
5
+ .controller('ListsCtrl', function($scope, List) {
6
+ $scope.list = List.new();
7
+
8
+ $scope.create = function() {
9
+ $scope.list.$save(function() {
10
+ $scope.list = List.new();
11
+ });
12
+ };
13
+
14
+ $scope.destroy = function(list) {
15
+ list.$destroy();
16
+ };
17
+
18
+ List.all(function(lists) {
19
+ $scope.$apply(function() {
20
+ $scope.lists = lists;
21
+ });
22
+ });
23
+ });