live_record 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -5
  3. data/.travis.yml +12 -0
  4. data/Gemfile +5 -1
  5. data/Gemfile.lock +199 -1
  6. data/README.md +38 -9
  7. data/app/assets/javascripts/live_record/plugins/live_dom.coffee +2 -3
  8. data/lib/live_record.rb +3 -0
  9. data/lib/live_record/.rspec +3 -0
  10. data/lib/live_record/{channel.rb → channel/implement.rb} +1 -4
  11. data/lib/live_record/config.ru +10 -0
  12. data/lib/live_record/generators/install_generator.rb +0 -6
  13. data/lib/live_record/generators/templates/live_record_channel.rb +1 -1
  14. data/lib/live_record/generators/templates/model.rb.rb +3 -0
  15. data/lib/live_record/model/callbacks.rb +36 -0
  16. data/lib/live_record/spec/factories/posts.rb +6 -0
  17. data/lib/live_record/spec/features/live_record_syncing_spec.rb +60 -0
  18. data/lib/live_record/spec/internal/app/assets/config/manifest.js +2 -0
  19. data/lib/live_record/spec/internal/app/assets/javascripts/application.js +17 -0
  20. data/lib/live_record/spec/internal/app/assets/javascripts/cable.js +12 -0
  21. data/lib/live_record/spec/internal/app/assets/javascripts/posts.coffee +14 -0
  22. data/lib/live_record/spec/internal/app/channels/application_cable/channel.rb +4 -0
  23. data/lib/live_record/spec/internal/app/channels/application_cable/connection.rb +8 -0
  24. data/lib/live_record/spec/internal/app/channels/live_record_channel.rb +4 -0
  25. data/lib/live_record/spec/internal/app/controllers/application_controller.rb +3 -0
  26. data/lib/live_record/spec/internal/app/controllers/posts_controller.rb +74 -0
  27. data/lib/live_record/spec/internal/app/models/application_record.rb +3 -0
  28. data/lib/live_record/spec/internal/app/models/live_record_update.rb +3 -0
  29. data/lib/live_record/spec/internal/app/models/post.rb +11 -0
  30. data/lib/live_record/spec/internal/app/views/layouts/application.html.erb +13 -0
  31. data/lib/live_record/spec/internal/app/views/posts/_form.html.erb +27 -0
  32. data/lib/live_record/spec/internal/app/views/posts/_post.json.jbuilder +2 -0
  33. data/lib/live_record/spec/internal/app/views/posts/edit.html.erb +6 -0
  34. data/lib/live_record/spec/internal/app/views/posts/index.html.erb +32 -0
  35. data/lib/live_record/spec/internal/app/views/posts/index.json.jbuilder +1 -0
  36. data/lib/live_record/spec/internal/app/views/posts/new.html.erb +5 -0
  37. data/lib/live_record/spec/internal/app/views/posts/show.html.erb +21 -0
  38. data/lib/live_record/spec/internal/app/views/posts/show.json.jbuilder +1 -0
  39. data/lib/live_record/spec/internal/config/cable.yml +8 -0
  40. data/lib/live_record/spec/internal/config/database.yml +3 -0
  41. data/lib/live_record/spec/internal/config/routes.rb +3 -0
  42. data/lib/live_record/spec/internal/db/schema.rb +16 -0
  43. data/lib/live_record/spec/internal/public/favicon.ico +0 -0
  44. data/lib/live_record/spec/rails_helper.rb +34 -0
  45. data/lib/live_record/spec/spec_helper.rb +12 -0
  46. data/lib/live_record/version.rb +1 -1
  47. data/live_record.gemspec +19 -3
  48. metadata +251 -8
  49. data/lib/live_record/model.rb +0 -36
@@ -1,4 +1,4 @@
1
1
  class LiveRecordChannel < ApplicationCable::Channel
2
2
  include ActiveSupport::Rescuable
3
- include LiveRecord::Channel
3
+ include LiveRecord::Channel::Implement
4
4
  end
@@ -9,6 +9,9 @@ class <%= class_name %> < <%= parent_class_name.classify %>
9
9
  <% if attributes.any?(&:password_digest?) -%>
10
10
  has_secure_password
11
11
  <% end -%>
12
+
13
+ include LiveRecord::Model::Callbacks
14
+ has_many :live_record_updates, as: :recordable
12
15
 
13
16
  def self.live_record_whitelisted_attributes(<%= class_name.underscore %>, current_user)
14
17
  # Add attributes to this array that you would like current_user to have access to.
@@ -0,0 +1,36 @@
1
+ module LiveRecord
2
+ module Model
3
+ module Callbacks
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_update :__live_record_reference_changed_attributes__
8
+ after_update_commit :__live_record_broadcast_record_update__
9
+ after_destroy_commit :__live_record_broadcast_record_destroy__
10
+
11
+ def self.live_record_whitelisted_attributes(record, current_user)
12
+ []
13
+ end
14
+
15
+ private
16
+
17
+ def __live_record_reference_changed_attributes__
18
+ @_live_record_changed_attributes = changed
19
+ end
20
+
21
+ def __live_record_broadcast_record_update__
22
+ included_attributes = attributes.slice(*@_live_record_changed_attributes)
23
+ @_live_record_changed_attributes = nil
24
+ message_data = { 'action' => 'update', 'attributes' => included_attributes }
25
+ LiveRecordChannel.broadcast_to(self, message_data)
26
+ LiveRecordUpdate.create!(recordable_type: self.class, recordable_id: self.id, created_at: DateTime.now)
27
+ end
28
+
29
+ def __live_record_broadcast_record_destroy__
30
+ message_data = { 'action' => 'destroy' }
31
+ LiveRecordChannel.broadcast_to(self, message_data)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ factory :post do
3
+ title { Faker::Lorem.sentence }
4
+ content { Faker::Lorem.paragraph }
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.feature 'LiveRecord Syncing', type: :feature do
4
+ let(:post1) { create(:post) }
5
+ let(:post2) { create(:post) }
6
+ let(:post3) { create(:post) }
7
+ let!(:posts) { [post1, post2, post3] }
8
+
9
+ scenario 'User sees live changes (updates) of post records', js: true do
10
+ visit '/posts'
11
+
12
+ post1_title_td = find('td', text: post1.title, wait: 10)
13
+ post2_title_td = find('td', text: post2.title, wait: 10)
14
+ post3_title_td = find('td', text: post3.title, wait: 10)
15
+
16
+ post1.update!(title: 'post1newtitle')
17
+ post2.update!(title: 'post2newtitle')
18
+
19
+ expect(post1_title_td).to have_content('post1newtitle', wait: 10)
20
+ expect(post2_title_td).to have_content('post2newtitle', wait: 10)
21
+ expect(post3_title_td).to have_content(post3.title, wait: 10)
22
+ end
23
+
24
+ scenario 'User sees live changes (destroy) post records', js: true do
25
+ visit '/posts'
26
+
27
+ expect{find('td', text: post1.title, wait: 10)}.to_not raise_error
28
+ expect{find('td', text: post2.title, wait: 10)}.to_not raise_error
29
+ expect{find('td', text: post3.title, wait: 10)}.to_not raise_error
30
+
31
+ post1.destroy
32
+ post2.destroy
33
+
34
+ expect{find('td', text: post1.title)}.to raise_error Capybara::ElementNotFound
35
+ expect{find('td', text: post2.title)}.to raise_error Capybara::ElementNotFound
36
+ expect{find('td', text: post3.title)}.to_not raise_error
37
+ end
38
+
39
+ scenario 'User sees live changes (updates) of post records, but only changes from whitelisted authorised attributes', js: true do
40
+ visit '/posts'
41
+
42
+ post1_title_td = find('td', text: post1.title, wait: 10)
43
+ post1_content_td = find('td', text: post1.content, wait: 10)
44
+ post2_title_td = find('td', text: post2.title, wait: 10)
45
+ post2_content_td = find('td', text: post2.content, wait: 10)
46
+ post3_title_td = find('td', text: post3.title, wait: 10)
47
+ post3_content_td = find('td', text: post3.content, wait: 10)
48
+
49
+ post1.update!(title: 'post1newtitle', content: 'post1newcontent')
50
+ post2.update!(title: 'post2newtitle', content: 'post2newcontent')
51
+ post3.update!(title: 'post3newtitle', content: 'post3newcontent')
52
+
53
+ expect(post1_title_td).to have_content('post1newtitle', wait: 10)
54
+ expect(post1_content_td).to_not have_content('post1newcontent')
55
+ expect(post2_title_td).to have_content('post2newtitle', wait: 10)
56
+ expect(post2_content_td).to_not have_content('post2newcontent')
57
+ expect(post3_title_td).to have_content('post3newtitle', wait: 10)
58
+ expect(post3_content_td).to_not have_content('post3newcontent')
59
+ end
60
+ end
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,17 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
5
+ // vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require rails-ujs
14
+ //= require jquery
15
+ //= require live_record
16
+ //= require live_record/plugins/live_dom
17
+ //= require_tree .
@@ -0,0 +1,12 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3
+ //
4
+ //= require action_cable
5
+ //= require_self
6
+
7
+ (function() {
8
+ this.App || (this.App = {});
9
+
10
+ App.cable = ActionCable.createConsumer();
11
+
12
+ }).call(this);
@@ -0,0 +1,14 @@
1
+ LiveRecord.Model.create(
2
+ {
3
+ modelName: 'Post',
4
+ plugins: {
5
+ LiveDOM: true
6
+ },
7
+ # See TODO: URL_TO_DOCUMENTATION for supported callbacks
8
+ # Add Callbacks (callback name => array of functions)
9
+ # callbacks: {
10
+ # 'on:disconnect': [],
11
+ # 'after:update': [],
12
+ # }
13
+ }
14
+ )
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ identified_by :current_user
4
+
5
+ def current_user
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ class LiveRecordChannel < ApplicationCable::Channel
2
+ include ActiveSupport::Rescuable
3
+ include LiveRecord::Channel::Implement
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ end
@@ -0,0 +1,74 @@
1
+ class PostsController < ApplicationController
2
+ before_action :set_post, only: [:show, :edit, :update, :destroy]
3
+
4
+ # GET /posts
5
+ # GET /posts.json
6
+ def index
7
+ @posts = Post.all
8
+ end
9
+
10
+ # GET /posts/1
11
+ # GET /posts/1.json
12
+ def show
13
+ end
14
+
15
+ # GET /posts/new
16
+ def new
17
+ @post = Post.new
18
+ end
19
+
20
+ # GET /posts/1/edit
21
+ def edit
22
+ end
23
+
24
+ # POST /posts
25
+ # POST /posts.json
26
+ def create
27
+ @post = Post.new(post_params)
28
+
29
+ respond_to do |format|
30
+ if @post.save
31
+ format.html { redirect_to @post, notice: 'Post was successfully created.' }
32
+ format.json { render :show, status: :created, location: @post }
33
+ else
34
+ format.html { render :new }
35
+ format.json { render json: @post.errors, status: :unprocessable_entity }
36
+ end
37
+ end
38
+ end
39
+
40
+ # PATCH/PUT /posts/1
41
+ # PATCH/PUT /posts/1.json
42
+ def update
43
+ respond_to do |format|
44
+ if @post.update(post_params)
45
+ format.html { redirect_to @post, notice: 'Post was successfully updated.' }
46
+ format.json { render :show, status: :ok, location: @post }
47
+ else
48
+ format.html { render :edit }
49
+ format.json { render json: @post.errors, status: :unprocessable_entity }
50
+ end
51
+ end
52
+ end
53
+
54
+ # DELETE /posts/1
55
+ # DELETE /posts/1.json
56
+ def destroy
57
+ @post.destroy
58
+ respond_to do |format|
59
+ format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' }
60
+ format.json { head :no_content }
61
+ end
62
+ end
63
+
64
+ private
65
+ # Use callbacks to share common setup or constraints between actions.
66
+ def set_post
67
+ @post = Post.find(params[:id])
68
+ end
69
+
70
+ # Never trust parameters from the scary internet, only allow the white list through.
71
+ def post_params
72
+ params.require(:post).permit(:title, :content)
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,3 @@
1
+ class LiveRecordUpdate < ApplicationRecord
2
+ belongs_to :recordable, polymorphic: true
3
+ end
@@ -0,0 +1,11 @@
1
+ class Post < ApplicationRecord
2
+ include LiveRecord::Model::Callbacks
3
+
4
+ has_many :live_record_updates, as: :recordable
5
+
6
+ def self.live_record_whitelisted_attributes(post, current_user)
7
+ # Add attributes to this array that you would like current_user to have access to.
8
+ # Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
9
+ [:title]
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>LiveRecordExample</title>
5
+ <%= csrf_meta_tags %>
6
+
7
+ <%= javascript_include_tag 'application' %>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,27 @@
1
+ <%= form_with(model: post, local: true) do |form| %>
2
+ <% if post.errors.any? %>
3
+ <div id="error_explanation">
4
+ <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
5
+
6
+ <ul>
7
+ <% post.errors.full_messages.each do |message| %>
8
+ <li><%= message %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="field">
15
+ <%= form.label :title %>
16
+ <%= form.text_field :title, id: :post_title %>
17
+ </div>
18
+
19
+ <div class="field">
20
+ <%= form.label :content %>
21
+ <%= form.text_area :content, id: :post_content %>
22
+ </div>
23
+
24
+ <div class="actions">
25
+ <%= form.submit %>
26
+ </div>
27
+ <% end %>
@@ -0,0 +1,2 @@
1
+ json.extract! post, :id, :title, :content, :created_at, :updated_at
2
+ json.url post_url(post, format: :json)
@@ -0,0 +1,6 @@
1
+ <h1>Editing Post</h1>
2
+
3
+ <%= render 'form', post: @post %>
4
+
5
+ <%= link_to 'Show', @post %> |
6
+ <%= link_to 'Back', posts_path %>
@@ -0,0 +1,32 @@
1
+ <script>
2
+ LiveRecord.helpers.loadRecords({modelName: 'Post'})
3
+ </script>
4
+ <p id="notice"><%= notice %></p>
5
+
6
+ <h1>Posts</h1>
7
+
8
+ <table>
9
+ <thead>
10
+ <tr>
11
+ <th>Title</th>
12
+ <th>Content</th>
13
+ <th colspan="3"></th>
14
+ </tr>
15
+ </thead>
16
+
17
+ <tbody>
18
+ <% @posts.each do |post| %>
19
+ <tr data-live-record-destroy-from='Post-<%= post.id %>'>
20
+ <td data-live-record-update-from='Post-<%= post.id %>-title'><%= post.title %></td>
21
+ <td data-live-record-update-from='Post-<%= post.id %>-content'><%= post.content %></td>
22
+ <td><%= link_to 'Show', post %></td>
23
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
24
+ <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+
30
+ <br>
31
+
32
+ <%= link_to 'New Post', new_post_path %>
@@ -0,0 +1 @@
1
+ json.array! @posts, partial: 'posts/post', as: :post
@@ -0,0 +1,5 @@
1
+ <h1>New Post</h1>
2
+
3
+ <%= render 'form', post: @post %>
4
+
5
+ <%= link_to 'Back', posts_path %>
@@ -0,0 +1,21 @@
1
+ <script>
2
+ LiveRecord.helpers.loadRecords({modelName: 'Post'})
3
+ </script>
4
+ <p id="notice"><%= notice %></p>
5
+
6
+ <p>
7
+ <strong>Title:</strong>
8
+ <span data-live-record-update-from='Post-<%= @post.id %>-title'>
9
+ <%= @post.title %>
10
+ </span>
11
+ </p>
12
+
13
+ <p>
14
+ <strong>Content:</strong>
15
+ <span data-live-record-update-from='Post-<%= @post.id %>-content'>
16
+ <%= @post.content %>
17
+ </span>
18
+ </p>
19
+
20
+ <%= link_to 'Edit', edit_post_path(@post) %> |
21
+ <%= link_to 'Back', posts_path %>
@@ -0,0 +1 @@
1
+ json.partial! "posts/post", post: @post
@@ -0,0 +1,8 @@
1
+ development:
2
+ adapter: async
3
+
4
+ test:
5
+ adapter: async
6
+
7
+ production:
8
+ adapter: async
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite