live_record 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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