databound 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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