active_record_api-rest 0.0.2

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.
@@ -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