databound 0.0.3

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +63 -0
  7. data/Rakefile +2 -0
  8. data/config.ru +7 -0
  9. data/databound.gemspec +31 -0
  10. data/lib/databound.rb +83 -0
  11. data/lib/databound/data.rb +51 -0
  12. data/lib/databound/manager.rb +60 -0
  13. data/lib/databound/rails/routes.rb +17 -0
  14. data/lib/databound/version.rb +3 -0
  15. data/spec/controllers/databound_spec.rb +138 -0
  16. data/spec/controllers/dsl_controller_spec.rb +177 -0
  17. data/spec/controllers/loose_dsl_controller_spec.rb +127 -0
  18. data/spec/controllers/no_model_controller_spec.rb +9 -0
  19. data/spec/controllers/permitted_columns_controller_spec.rb +104 -0
  20. data/spec/internal/app/controllers/application_controller.rb +5 -0
  21. data/spec/internal/app/controllers/dsl_controller.rb +17 -0
  22. data/spec/internal/app/controllers/loose_dsl_controller.rb +13 -0
  23. data/spec/internal/app/controllers/no_model_controller.rb +3 -0
  24. data/spec/internal/app/controllers/permitted_columns_controller.rb +13 -0
  25. data/spec/internal/app/controllers/users_controller.rb +9 -0
  26. data/spec/internal/app/models/user.rb +2 -0
  27. data/spec/internal/config/database.yml +3 -0
  28. data/spec/internal/config/routes.rb +7 -0
  29. data/spec/internal/db/combustion_test.sqlite +0 -0
  30. data/spec/internal/db/schema.rb +7 -0
  31. data/spec/internal/log/.gitignore +1 -0
  32. data/spec/internal/public/favicon.ico +0 -0
  33. data/spec/spec_helper.rb +36 -0
  34. data/spec/support/rails_test_app/.gitignore +16 -0
  35. data/spec/support/rails_test_app/Gemfile +40 -0
  36. data/spec/support/rails_test_app/Gemfile.lock +120 -0
  37. data/spec/support/rails_test_app/README.rdoc +28 -0
  38. data/spec/support/rails_test_app/Rakefile +6 -0
  39. data/spec/support/rails_test_app/app/assets/images/.keep +0 -0
  40. data/spec/support/rails_test_app/app/assets/javascripts/application.js +16 -0
  41. data/spec/support/rails_test_app/app/assets/stylesheets/application.css +15 -0
  42. data/spec/support/rails_test_app/app/controllers/application_controller.rb +5 -0
  43. data/spec/support/rails_test_app/app/controllers/concerns/.keep +0 -0
  44. data/spec/support/rails_test_app/app/helpers/application_helper.rb +2 -0
  45. data/spec/support/rails_test_app/app/mailers/.keep +0 -0
  46. data/spec/support/rails_test_app/app/models/.keep +0 -0
  47. data/spec/support/rails_test_app/app/models/concerns/.keep +0 -0
  48. data/spec/support/rails_test_app/app/views/layouts/application.html.erb +14 -0
  49. data/spec/support/rails_test_app/bin/bundle +3 -0
  50. data/spec/support/rails_test_app/bin/rails +8 -0
  51. data/spec/support/rails_test_app/bin/rake +8 -0
  52. data/spec/support/rails_test_app/bin/spring +18 -0
  53. data/spec/support/rails_test_app/config.ru +4 -0
  54. data/spec/support/rails_test_app/config/application.rb +30 -0
  55. data/spec/support/rails_test_app/config/boot.rb +4 -0
  56. data/spec/support/rails_test_app/config/database.yml +25 -0
  57. data/spec/support/rails_test_app/config/environment.rb +5 -0
  58. data/spec/support/rails_test_app/config/environments/development.rb +37 -0
  59. data/spec/support/rails_test_app/config/environments/production.rb +78 -0
  60. data/spec/support/rails_test_app/config/environments/test.rb +39 -0
  61. data/spec/support/rails_test_app/config/initializers/assets.rb +8 -0
  62. data/spec/support/rails_test_app/config/initializers/backtrace_silencers.rb +7 -0
  63. data/spec/support/rails_test_app/config/initializers/cookies_serializer.rb +3 -0
  64. data/spec/support/rails_test_app/config/initializers/filter_parameter_logging.rb +4 -0
  65. data/spec/support/rails_test_app/config/initializers/inflections.rb +16 -0
  66. data/spec/support/rails_test_app/config/initializers/mime_types.rb +4 -0
  67. data/spec/support/rails_test_app/config/initializers/session_store.rb +3 -0
  68. data/spec/support/rails_test_app/config/initializers/wrap_parameters.rb +14 -0
  69. data/spec/support/rails_test_app/config/locales/en.yml +23 -0
  70. data/spec/support/rails_test_app/config/routes.rb +56 -0
  71. data/spec/support/rails_test_app/config/secrets.yml +22 -0
  72. data/spec/support/rails_test_app/db/seeds.rb +7 -0
  73. data/spec/support/rails_test_app/lib/assets/.keep +0 -0
  74. data/spec/support/rails_test_app/lib/tasks/.keep +0 -0
  75. data/spec/support/rails_test_app/log/.keep +0 -0
  76. data/spec/support/rails_test_app/public/404.html +67 -0
  77. data/spec/support/rails_test_app/public/422.html +67 -0
  78. data/spec/support/rails_test_app/public/500.html +66 -0
  79. data/spec/support/rails_test_app/public/favicon.ico +0 -0
  80. data/spec/support/rails_test_app/public/robots.txt +5 -0
  81. data/spec/support/rails_test_app/vendor/assets/javascripts/.keep +0 -0
  82. data/spec/support/rails_test_app/vendor/assets/stylesheets/.keep +0 -0
  83. metadata +320 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 22275b2aa2e135427ffb2cccc53091c913706ef5
4
+ data.tar.gz: 93ad22a8162a45950b0bee675b03a098c6fa663c
5
+ SHA512:
6
+ metadata.gz: 1e9ae816ef58f24a27a57cdc819fcaea49b250ae8bf9feffca6c4fa7b998c1f1629863b044a6f27bf60bff641c9df200f7b372a72f3529acb20483e7f45be6be
7
+ data.tar.gz: 27dbd28d0751ba595b92836eb2c271a7a45a8b127cf31a04de620dc79c08044485635713824fe373dc0cdc123657e07b68a3653d5fb2de09671d96f87f978398
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.3
4
+ - 2.0.0
5
+ script: bundle exec rspec --pattern "spec/**/*_spec.rb"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in databound.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Domas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,63 @@
1
+ [![Code Climate](https://codeclimate.com/github/Nedomas/databound/badges/gpa.svg)](https://codeclimate.com/github/Nedomas/databound-rails)
2
+ [![Gem Version](https://badge.fury.io/rb/databound.svg)](http://badge.fury.io/rb/databound)
3
+ [![Build Status](https://travis-ci.org/Nedomas/databound.svg?branch=master)](https://travis-ci.org/Nedomas/databound-rails)
4
+ [![Dependency Status](https://gemnasium.com/Nedomas/databound.svg)](https://gemnasium.com/Nedomas/databound-rails)
5
+
6
+ ![Databound](https://cloud.githubusercontent.com/assets/1877286/4743542/df89dcec-5a28-11e4-9114-6f383fe269cb.png)
7
+
8
+ Exposes ActiveRecord records to the Javascript side.
9
+
10
+ This is the **Ruby on Rails** backend part for the ``Databound`` javascript lib.
11
+
12
+ For more information go to [javascript Databound repo](https://github.com/Nedomas/databound).
13
+
14
+ ## Javascript library
15
+
16
+ It does something like this out of the box.
17
+
18
+ ```js
19
+ User = new Databound('/users');
20
+
21
+ User.update({ id: 15, name: 'Saint John' }).then(function(updated_user) {
22
+ });
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ The library has two parts and has Lodash as a dependency.
28
+
29
+ #### I. Javascript part
30
+
31
+ Follow the guide on [javascript Databound repo](https://github.com/Nedomas/databound).
32
+
33
+ #### II. Ruby on Rails part
34
+
35
+ **1.** Add ``gem 'databound'`` to ``Gemfile``.
36
+
37
+ **2.** Create a controller with method ``model`` which returns the model to be accessed.
38
+ Also include ``Databound``
39
+
40
+ ```ruby
41
+ class UsersController < ApplicationController
42
+ include Databound
43
+
44
+ private
45
+
46
+ def model
47
+ User
48
+ end
49
+ end
50
+ ```
51
+
52
+ **3.** Add a route to ``routes.rb``
53
+
54
+ ```ruby
55
+ # This creates POST routes on /users to UsersController
56
+ # For where, create, update, destroy
57
+
58
+ databound :users
59
+ ```
60
+
61
+ ## Additional features
62
+
63
+ All features are described in [javascript Databound repo](https://github.com/Nedomas/databound).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :all
7
+ run Combustion::Application
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'databound/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'databound'
8
+ spec.version = Databound::VERSION
9
+ spec.authors = ['Domas Bitvinskas']
10
+ spec.email = ['domas.bitvinskas@me.com']
11
+ spec.summary = %q{ActiveRecord exposed to the Javascript side and guarded by guns}
12
+ spec.description = %q{This is the Ruby on Rails backend part for the Databound javascript lib.}
13
+ spec.homepage = 'https://github.com/Nedomas/databound'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'andand'
22
+ spec.add_development_dependency 'rspec-rails'
23
+ spec.add_development_dependency 'combustion', '~> 0.5.2'
24
+ spec.add_development_dependency 'rails'
25
+ spec.add_development_dependency 'sqlite3'
26
+
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'pry-stack_explorer'
29
+ spec.add_development_dependency 'bundler', '~> 1.6'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ end
@@ -0,0 +1,83 @@
1
+ require 'andand'
2
+
3
+ require 'databound/version'
4
+ require 'databound/data'
5
+ require 'databound/manager'
6
+ require 'databound/rails/routes'
7
+
8
+ module Databound
9
+ def self.included(base)
10
+ base.send(:before_action, :init_crud, only: %i(where create update destroy))
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ def where
15
+ records = @crud.find_scoped_records
16
+
17
+ render json: serialized(records)
18
+ end
19
+
20
+ def create
21
+ record = @crud.create_from_data
22
+
23
+ render json: {
24
+ success: true,
25
+ id: record.id,
26
+ }
27
+ end
28
+
29
+ def update
30
+ record = @crud.update_from_data
31
+
32
+ render json: {
33
+ success: true,
34
+ id: record.id,
35
+ }
36
+ end
37
+
38
+ def destroy
39
+ @crud.destroy_from_data
40
+
41
+ render json: {
42
+ success: true,
43
+ }
44
+ end
45
+
46
+ private
47
+
48
+ def serialized(records)
49
+ return records unless defined?(ActiveModel::Serializer)
50
+
51
+ serializer = ActiveModel::Serializer.serializer_for(records.first)
52
+ return records unless serializer
53
+
54
+ ActiveModel::ArraySerializer.new(records).to_json
55
+ end
56
+
57
+ def model
58
+ raise 'Override model method to specify a model to be used in CRUD'
59
+ end
60
+
61
+ def permitted_columns
62
+ # permit all by default
63
+ model.column_names
64
+ end
65
+
66
+ def init_crud
67
+ @crud = Databound::Manager.new(self)
68
+ end
69
+
70
+ module ClassMethods
71
+ attr_reader :dsls
72
+ attr_reader :stricts
73
+
74
+ def dsl(name, value, strict: true, &block)
75
+ @stricts ||= {}
76
+ @stricts[name.to_s] = strict
77
+
78
+ @dsls ||= {}
79
+ @dsls[name.to_s] ||= {}
80
+ @dsls[name.to_s][value.to_s] = block
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,51 @@
1
+ module Databound
2
+ class Data
3
+ def initialize(controller, json)
4
+ return unless json
5
+
6
+ @controller = controller
7
+ @params = JSON.parse(json) if json.is_a?(String)
8
+ @params = json if json.is_a?(Hash)
9
+ @data = interpolated_params
10
+ end
11
+
12
+ def records(model)
13
+ model.where(@data)
14
+ end
15
+
16
+ def to_h
17
+ @data
18
+ end
19
+
20
+ private
21
+
22
+ def interpolated_params
23
+ @params.each_with_object({}) do |(key, val), obj|
24
+ check_strict!(key, val)
25
+
26
+ block = dsl_block(key, val)
27
+ obj[key] = block ? block.call(@params.to_options) : val
28
+ end
29
+ end
30
+
31
+ def dsl_block(key, val)
32
+ dsl_key(key).andand[val]
33
+ end
34
+
35
+ def dsl_key(key)
36
+ @controller.class.dsls.andand[key]
37
+ end
38
+
39
+ def check_strict!(key, val)
40
+ return unless dsl_key(key)
41
+ return unless strict?(key) and !dsl_block(key, val)
42
+
43
+ raise NotPermittedError, "DSL column '#{key}' received unmatched string '#{val}'." \
44
+ " Use 'strict: false' in DSL definition to allow everything."
45
+ end
46
+
47
+ def strict?(key)
48
+ @controller.class.stricts.andand[key]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ module Databound
2
+ class NotPermittedError < RuntimeError; end
3
+ class Manager
4
+ def initialize(controller)
5
+ @model = controller.send(:model)
6
+ @permitted_columns = controller.send(:permitted_columns)
7
+
8
+ scope_js = controller.params[:scope]
9
+ data_js = controller.params[:data]
10
+ extra_find_scopes_js = controller.params[:extra_find_scopes] || '[]'
11
+
12
+ @scope = Databound::Data.new(controller, scope_js)
13
+ @data = Databound::Data.new(controller, data_js).to_h
14
+
15
+ @extra_find_scopes = JSON.parse(extra_find_scopes_js).map do |extra_scope|
16
+ Databound::Data.new(controller, extra_scope)
17
+ end
18
+ end
19
+
20
+ def find_scoped_records
21
+ records = []
22
+ records << @scope.records(@model)
23
+
24
+ @extra_find_scopes.each do |extra_scope|
25
+ records << extra_scope.records(@model)
26
+ end
27
+
28
+ records.map { |record| record.where(@data) }.flatten
29
+ end
30
+
31
+ def create_from_data
32
+ check_params!
33
+ @model.where(@scope.to_h).create(@data)
34
+ end
35
+
36
+ def update_from_data
37
+ id = @data.delete('id')
38
+
39
+ check_params!
40
+ record = @model.find(id)
41
+ record.update(@data)
42
+
43
+ record
44
+ end
45
+
46
+ def destroy_from_data
47
+ @model.find(@data['id']).destroy
48
+ end
49
+
50
+ private
51
+
52
+ def check_params!
53
+ requested = [@scope, @data].map(&:to_h).flat_map(&:keys)
54
+ unpermitted = requested - @permitted_columns.map(&:to_s)
55
+ return if unpermitted.empty?
56
+
57
+ raise NotPermittedError, "Request includes unpermitted columns: #{unpermitted.join(', ')}"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ class ActionDispatch::Routing::Mapper
2
+ def databound(*resources)
3
+ namespace = @scope[:path]
4
+ namespace = namespace[1..-1] if namespace
5
+
6
+ resources.each do |resource|
7
+ Rails.application.routes.draw do
8
+ %i(where create update destroy).each do |name|
9
+ path = [namespace, resource, name].compact.join('/')
10
+ controller = [namespace, resource].compact.join('/')
11
+ to = [controller, name].join('#')
12
+ post path => to
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Databound
2
+ VERSION = '0.0.3'
3
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe UsersController, type: :controller do
4
+ describe '#create' do
5
+ before :each do
6
+ data = {
7
+ data: {
8
+ name: 'John',
9
+ },
10
+ scope: {},
11
+ extra_find_scopes: [],
12
+ }
13
+
14
+ post(:create, javascriptize(data))
15
+ end
16
+
17
+ it 'responds consistently to js' do
18
+ expect(rubize(response)).to eq(success: true, id: 1)
19
+ end
20
+
21
+ it 'creates the record' do
22
+ user = User.find(1)
23
+ user_attributes = user.attributes.to_options
24
+ expect(user_attributes.slice(:id, :name)).to eq(id: 1, name: 'John')
25
+ end
26
+ end
27
+
28
+ describe '#where' do
29
+ before :each do
30
+ User.create(name: 'John', city: 'New York')
31
+ User.create(name: 'Peter', city: 'New York')
32
+ User.create(name: 'Nikki', city: 'Hollywood')
33
+ end
34
+
35
+ it 'respond with empty records' do
36
+ data = {
37
+ data: {
38
+ city: 'Los Angeles',
39
+ },
40
+ scope: {},
41
+ extra_find_scopes: [],
42
+ }
43
+
44
+ post(:where, javascriptize(data))
45
+ expect(rubize(response)).to eq([])
46
+ end
47
+
48
+ it 'respond with correct records' do
49
+ data = {
50
+ data: {
51
+ city: 'New York',
52
+ },
53
+ scope: {},
54
+ extra_find_scopes: [],
55
+ }
56
+
57
+ post(:where, javascriptize(data))
58
+ expect(gather(:name, response)).to eq(%w(John Peter))
59
+ end
60
+ end
61
+
62
+ describe '#update' do
63
+ before :each do
64
+ @user = User.create(name: 'John', city: 'New York')
65
+ end
66
+
67
+ describe 'update record correctly' do
68
+ before :each do
69
+ data = {
70
+ data: {
71
+ id: @user.id,
72
+ city: 'Moved to Los Angeles',
73
+ },
74
+ scope: {},
75
+ extra_find_scopes: [],
76
+ }
77
+
78
+ post(:update, javascriptize(data))
79
+ end
80
+
81
+ it 'respond with updated record id' do
82
+ expect(rubize(response)).to eq(success: true, id: @user.id)
83
+ end
84
+
85
+ it 'do the update' do
86
+ expect(@user.reload.city).to eq('Moved to Los Angeles')
87
+ end
88
+ end
89
+
90
+ it 'respond with error when id is missing' do
91
+ data = {
92
+ data: {
93
+ city: 'Moved to Los Angeles',
94
+ },
95
+ scope: {},
96
+ extra_find_scopes: [],
97
+ }
98
+
99
+ expect { post(:update, javascriptize(data)) }.to raise_error(ActiveRecord::RecordNotFound)
100
+ end
101
+ end
102
+
103
+ describe '#destroy' do
104
+ before :each do
105
+ @user = User.create(name: 'John', city: 'New York')
106
+ end
107
+
108
+ describe 'destroy record correctly' do
109
+ before :each do
110
+ data = {
111
+ data: {
112
+ id: @user.id,
113
+ },
114
+ scope: {},
115
+ extra_find_scopes: [],
116
+ }
117
+
118
+ post(:destroy, javascriptize(data))
119
+ end
120
+
121
+ it 'respond with success' do
122
+ expect(rubize(response)).to eq(success: true)
123
+ end
124
+ end
125
+
126
+ it 'respond with error when id is missing' do
127
+ data = {
128
+ data: {
129
+ id: 2,
130
+ },
131
+ scope: {},
132
+ extra_find_scopes: [],
133
+ }
134
+
135
+ expect { post(:update, javascriptize(data)) }.to raise_error(ActiveRecord::RecordNotFound)
136
+ end
137
+ end
138
+ end