active_record_api-rest 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record_api/rest/version'
4
+ require 'active_support'
5
+ require 'action_controller'
6
+ require 'active_record'
7
+ require 'cancancan'
8
+ require 'active_model_serializers'
9
+ require 'active_attr'
10
+
11
+ module ActiveRecordApi
12
+ module Rest
13
+ end
14
+ end
15
+
16
+ require_relative 'active_record_api/rest/railtie'
17
+ require_relative 'active_record_api/rest/request_url_generator'
@@ -0,0 +1,17 @@
1
+ module ActiveRecordApi
2
+ module Rest
3
+ module GracefulErrors
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ rescue_from ActiveRecord::RecordNotFound do |exception|
8
+ render status: :not_found, json: { base: exception.message }
9
+ end
10
+
11
+ rescue_from CanCan::AccessDenied do |exception|
12
+ render status: :forbidden, json: { base: "Access denied on #{exception.action} #{exception.subject.inspect}" }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ require_relative 'index_controller'
2
+
3
+ module ActiveRecordApi
4
+ module Rest
5
+ class Controller < IndexController
6
+ before_action :initialize_model, only: :create
7
+ before_action :only_valid_params, only: %i[create update]
8
+
9
+ def show
10
+ return unless stale?(last_modified: model.updated_at)
11
+ render json: model, serializer: serializer
12
+ end
13
+
14
+ def create
15
+ if model.save
16
+ redirect_to_model
17
+ else
18
+ render json: model.errors, status: :unprocessable_entity
19
+ end
20
+ end
21
+
22
+ def update
23
+ if model.update(resource_params)
24
+ redirect_to_model
25
+ else
26
+ render json: model.errors, status: :unprocessable_entity
27
+ end
28
+ end
29
+
30
+ def destroy
31
+ model.destroy!
32
+ head :no_content
33
+ end
34
+
35
+ protected
36
+
37
+ def redirect_to_model
38
+ host = RequestUrlGenerator.new(request: request).micro_service_url
39
+ relative_url = url_for(controller: controller_name, action: 'show', id: model.id, only_path: true)
40
+ redirect_to "#{host}#{relative_url}", status: :see_other
41
+ end
42
+
43
+ def only_valid_params
44
+ return unless not_allowed_params.present?
45
+ render json: { base: "Extra parameters are not allow: #{not_allowed_params.join(', ')}" }, status: :unprocessable_entity
46
+ end
47
+
48
+ def resource_params
49
+ @resource_params ||= filtered_params.reject do |column_name|
50
+ column_name == :id
51
+ end
52
+ end
53
+
54
+ def not_allowed_params
55
+ @not_allowed_params ||= ((params.keys.map(&:to_sym) - [:controller, :action, model_klass.name.underscore.to_sym]) - resource_params.keys.map(&:to_sym))
56
+ end
57
+
58
+ def initialize_model
59
+ instance_variable_set("@#{controller_name.singularize}", model_klass.new(filtered_params))
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'concerns/graceful_errors'
2
+
3
+ module ActiveRecordApi
4
+ module Rest
5
+ class IndexController < ApplicationController
6
+ include CanCan::ControllerAdditions
7
+
8
+ include GracefulErrors
9
+
10
+ load_and_authorize_resource
11
+ before_action :load_models, only: :index
12
+
13
+ def index
14
+ response.headers['x-total'] = @total
15
+ response.headers['x-link-next'] = next_link
16
+ render json: models, each_serializer: serializer
17
+ end
18
+
19
+ protected
20
+
21
+ def next_link
22
+ return unless @remaining_count > limit
23
+ new_params = request.query_parameters.dup
24
+ new_params['previous_id'] = models.last.id
25
+ RequestUrlGenerator.new(request: request, new_params: new_params).url
26
+ end
27
+
28
+ def serializer
29
+ "#{model_klass.name}Serializer".safe_constantize
30
+ end
31
+
32
+ def allowed_params
33
+ @allowed_params ||= model_klass.column_names.map do |column_name|
34
+ column_name.to_s.gsub('encrypted_', '')
35
+ end.map(&:to_sym)
36
+ end
37
+
38
+ def filtered_params
39
+ @filtered_params ||= params.permit(allowed_params).to_h
40
+ end
41
+
42
+ def model
43
+ instance_variable_get("@#{controller_name.singularize}")
44
+ end
45
+
46
+ def load_models
47
+ filter_models
48
+ @total = models.count
49
+ page_models
50
+ end
51
+
52
+ def filter_models
53
+ models models.where(filtered_params)
54
+ end
55
+
56
+ def page_models
57
+ id_field = model_klass.arel_table[:id]
58
+ models models.order(:id).where(id_field.gt previous_id)
59
+ @remaining_count = models.count
60
+ models models.limit(limit)
61
+ end
62
+
63
+ def models(updated_models = nil)
64
+ if updated_models.nil?
65
+ instance_variable_get("@#{controller_name}")
66
+ else
67
+ instance_variable_set("@#{controller_name}", updated_models)
68
+ end
69
+ end
70
+
71
+ def model_klass
72
+ @model_klass ||= controller_name.classify.constantize
73
+ end
74
+
75
+ def limit
76
+ @limit ||= params[:limit]&.to_i || 50
77
+ end
78
+
79
+ def previous_id
80
+ @previous_id ||= params[:previous_id] || 0
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+
5
+ #:nocov:#
6
+ module ActiveRecordApi
7
+ module Rest
8
+ class Railtie < Rails::Railtie
9
+ initializer 'load order fix for application controller' do
10
+ require_relative 'controller'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ #:nocov:#
@@ -0,0 +1,29 @@
1
+ module ActiveRecordApi
2
+ module Rest
3
+ class RequestUrlGenerator
4
+ include ActiveAttr::Model
5
+
6
+ attribute :request
7
+ attribute :new_params, default: {}
8
+
9
+ delegate :host, :path, to: :request, prefix: true
10
+
11
+ def url
12
+ "#{micro_service_url}#{request_path}?#{new_params.to_param}"
13
+ end
14
+
15
+ def micro_service_url
16
+ "#{protocol}#{request_host}/api/#{micro_service_name}"
17
+ end
18
+
19
+ def micro_service_name
20
+ Rails.application.class.parent.to_s.underscore.tr('_', '-')
21
+ end
22
+
23
+ def protocol
24
+ return 'http://' if Rails.env.development?
25
+ 'https://'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_record_api-rest'
2
+
3
+ require_relative 'spec/rest_controller_shared_example'
@@ -0,0 +1,213 @@
1
+ shared_examples 'all::rest::actions' do
2
+ let(:authorize_read) { mock_valid_auth(permission_name(model_klass, 'r'), model.organization_id) }
3
+ let(:unauthorize_read) { mock_valid_auth(permission_name(model_klass, ''), -1) }
4
+ let(:authorize_create) { mock_valid_auth(permission_name(model_klass, 'c'), model.organization_id) }
5
+ let(:unauthorize_create) { mock_valid_auth(permission_name(model_klass, ''), -1) }
6
+ let(:authorize_update) { mock_valid_auth(permission_name(model_klass, 'u'), model.organization_id) }
7
+ let(:unauthorize_update) { mock_valid_auth(permission_name(model_klass, ''), -1) }
8
+ let(:authorize_delete) { mock_valid_auth(permission_name(model_klass, 'd'), model.organization_id) }
9
+ let(:unauthorize_delete) { mock_valid_auth(permission_name(model_klass, ''), -1) }
10
+ let(:factory_symbol) { model_klass.to_s.underscore.to_sym }
11
+ let(:index_non_accessible_data) { create factory_symbol, organization_id: (model.organization_id + 1) }
12
+ let(:model_array) { [model] + create_list(factory_symbol, 4, organization_id: model.organization_id) }
13
+ describe 'All rest actions' do
14
+ let(:serializer) { ActiveModelSerializers::SerializableResource }
15
+ let(:model_klass) { described_class.controller_name.classify.constantize }
16
+ let(:model) { create model_klass.to_s.underscore.to_sym }
17
+ include_examples 'get::show'
18
+ include_examples 'get::index'
19
+ include_examples 'put::update'
20
+ include_examples 'post::create'
21
+ include_examples 'delete::delete'
22
+ end
23
+ end
24
+
25
+ shared_examples 'get::show' do
26
+ describe 'GET show' do
27
+ context 'when authorized' do
28
+ before(:each) do
29
+ authorize_read
30
+ end
31
+ context 'when valid' do
32
+ before(:each) do
33
+ get :show, params: { id: model.to_param }
34
+ end
35
+ it { expect(response.status).to eq 200 }
36
+ it { expect(response.body).to be_json_eql(serializer.new(model).to_json) }
37
+ end
38
+ context 'when not found' do
39
+ before(:each) do
40
+ get :show, params: { id: -1 }
41
+ end
42
+ it { expect(response.status).to eq 404 }
43
+ it { expect(JSON.parse(response.body)['base']).to include "Couldn't find #{model_klass} with 'id'=-1" }
44
+ end
45
+ end
46
+ context 'when not authorized' do
47
+ before(:each) do
48
+ unauthorize_read
49
+ get :show, params: { id: model.to_param }
50
+ end
51
+ it { expect(response.status).to eq 403 }
52
+ it { expect(JSON.parse(response.body)['base']).to include 'Access denied on show' }
53
+ end
54
+ end
55
+ end
56
+
57
+ shared_examples 'get::index' do
58
+ describe 'GET index' do
59
+ context 'when authorized' do
60
+ before(:each) do
61
+ authorize_read
62
+ end
63
+ context 'when one result' do
64
+ before(:each) do
65
+ model
66
+ get :index
67
+ end
68
+ it { expect(response.status).to eq 200 }
69
+ it { expect(response.headers['x-total']).to eq 1 }
70
+ it { expect(response.headers['x-link-next']).to be nil }
71
+ end
72
+
73
+ context 'when paged results' do
74
+ let(:previous_id) { model_klass.limit(1).pluck(:id).first }
75
+ let(:next_previous_id) { model_klass.where('id > ?', previous_id).limit(1).pluck(:id).last }
76
+ before(:each) do
77
+ model_array
78
+ get :index, params: { previous_id: previous_id, limit: 1 }
79
+ end
80
+ it { expect(response.status).to eq 200 }
81
+ it { expect(response.headers['x-total']).to eq model_array.length }
82
+ it { expect(response.headers['x-link-next']).to eq "https://test.host/api/#{Rails.application.class.parent.to_s.underscore}#{request.path}?limit=1&previous_id=#{next_previous_id}" }
83
+ it { expect(response.body).to be_json_eql model_klass.where('id > ?', previous_id).limit(1).map { |o| serializer.new(o) }.to_json }
84
+ end
85
+
86
+ context 'when only access some of the data' do
87
+ before(:each) do
88
+ index_non_accessible_data
89
+ get :index
90
+ end
91
+ it { expect(response.status).to eq 200 }
92
+ it { expect(response.headers['x-total']).to eq 1 }
93
+ it { expect(response.body).to be_json_eql [serializer.new(model)].to_json }
94
+ end
95
+ end
96
+ context 'when not authorized' do
97
+ before(:each) do
98
+ unauthorize_read
99
+ end
100
+ before(:each) do
101
+ get :index
102
+ end
103
+ it { expect(response.status).to eq 403 }
104
+ it { expect(JSON.parse(response.body)['base']).to include 'Access denied on index' }
105
+ end
106
+ end
107
+ end
108
+
109
+ shared_examples 'put::update' do
110
+ let(:payload) { model.clone.attributes }
111
+ let(:host) { ActiveRecordApi::Rest::RequestUrlGenerator.new(request: request).micro_service_url }
112
+ let(:relative_url) { Rails.application.routes.url_helpers.url_for(controller: model_klass.to_s.underscore.pluralize, action: 'show', id: model.id, only_path: true) }
113
+ describe 'PUT update' do
114
+ context 'when authorized' do
115
+ before(:each) do
116
+ authorize_update
117
+ end
118
+ context 'valid' do
119
+ before(:each) do
120
+ put :update, params: payload
121
+ end
122
+ it { expect(response.status).to eq 303 }
123
+ it { expect(response).to redirect_to "#{host}#{relative_url}" }
124
+ end
125
+ context 'when invalid param values provided' do
126
+ before(:each) do
127
+ put :update, params: update_invalid_model.attributes.merge(id: model.id)
128
+ expect(update_invalid_model.valid?).to be false
129
+ end
130
+ it { expect(response.status).to eq(422) }
131
+ it { expect(response.body).to be_json_eql(update_invalid_model.errors.to_json) }
132
+ end
133
+ context 'when invalid param keys provided' do
134
+ before(:each) do
135
+ put :update, params: model.attributes.merge('foobars' => 'stuff')
136
+ end
137
+ it { expect(response.status).to eq(422) }
138
+ it { expect(response.body).to be_json_eql({ base: 'Extra parameters are not allow: foobars' }.to_json) }
139
+ end
140
+ end
141
+ context 'when not authorized' do
142
+ before(:each) do
143
+ unauthorize_update
144
+ get :show, params: payload
145
+ end
146
+ it { expect(response.status).to eq 403 }
147
+ it { expect(JSON.parse(response.body)['base']).to include 'Access denied on show' }
148
+ end
149
+ end
150
+ end
151
+
152
+ shared_examples 'post::create' do
153
+ let(:new_attributes) { model.dup.attributes }
154
+ let(:model_id) { model.id }
155
+ let(:host) { ActiveRecordApi::Rest::RequestUrlGenerator.new(request: request).micro_service_url }
156
+ let(:relative_url) { Rails.application.routes.url_helpers.url_for(controller: model_klass.to_s.underscore.pluralize, action: 'show', id: model_klass.last.id, only_path: true) }
157
+ describe 'POST create' do
158
+ context 'when authorized' do
159
+ before(:each) do
160
+ authorize_create
161
+ end
162
+ context 'when invalid' do
163
+ before(:each) do
164
+ expect_any_instance_of(model_klass).to receive(:save).and_return(false)
165
+ post :create, params: new_attributes
166
+ end
167
+ it { expect(response.status).to eq 422 }
168
+ end
169
+ context 'when valid' do
170
+ it 'creates record with attributes' do
171
+ model.delete
172
+ expect {
173
+ post :create, params: new_attributes
174
+ expect(response).to redirect_to "#{host}#{relative_url}"
175
+ }.to change(model_klass, :count).by(1)
176
+ expect(model_klass.last.id).not_to eq model_id
177
+ end
178
+ end
179
+ end
180
+ context 'when not authorized' do
181
+ before(:each) do
182
+ unauthorize_create
183
+ post :create, params: new_attributes
184
+ end
185
+ it { expect(response.status).to eq 403 }
186
+ it { expect(JSON.parse(response.body)['base']).to include 'Access denied on create' }
187
+ end
188
+ end
189
+ end
190
+
191
+ shared_examples 'delete::delete' do
192
+ describe 'DELETE delete' do
193
+ context 'when authorized' do
194
+ before(:each) do
195
+ authorize_delete
196
+ end
197
+ it 'remove record' do
198
+ expect {
199
+ expect_any_instance_of(model_klass).to receive(:destroyed_bus_cleanup_message_publish) if model.respond_to? :destroyed_bus_cleanup_message_publish
200
+ delete :destroy, params: { id: model.id }
201
+ }.to change(model_klass, :count).by(-1)
202
+ end
203
+ end
204
+ context 'when not authorized' do
205
+ before(:each) do
206
+ unauthorize_delete
207
+ delete :destroy, params: { id: model.id }
208
+ end
209
+ it { expect(response.status).to eq 403 }
210
+ it { expect(JSON.parse(response.body)['base']).to include 'Access denied on destroy' }
211
+ end
212
+ end
213
+ end