apicasso 0.2.16 → 0.3.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.
- checksums.yaml +5 -5
- data/README.md +176 -176
- data/Rakefile +32 -32
- data/app/controllers/apicasso/application_controller.rb +124 -124
- data/app/controllers/apicasso/crud_controller.rb +211 -211
- data/app/controllers/concerns/orderable.rb +45 -45
- data/app/models/apicasso/ability.rb +38 -40
- data/app/models/apicasso/application_record.rb +6 -6
- data/app/models/apicasso/key.rb +25 -25
- data/app/models/apicasso/request.rb +8 -8
- data/config/routes.rb +14 -14
- data/db/migrate/20180826141433_create_apicasso_tables.rb +17 -17
- data/lib/apicasso/engine.rb +6 -6
- data/lib/apicasso/version.rb +3 -3
- data/lib/apicasso.rb +9 -9
- data/lib/generators/apicasso/install/install_generator.rb +25 -25
- data/lib/generators/apicasso/install/templates/create_apicasso_tables.rb +17 -17
- data/lib/tasks/apicasso_tasks.rake +4 -4
- metadata +2 -2
@@ -1,124 +1,124 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
# Controller to extract common API features,
|
5
|
-
# such as authentication and authorization
|
6
|
-
class ApplicationController < ActionController::API
|
7
|
-
include ActionController::HttpAuthentication::Token::ControllerMethods
|
8
|
-
prepend_before_action :restrict_access, unless: -> { preflight? }
|
9
|
-
before_action :set_access_control_headers
|
10
|
-
after_action :register_api_request
|
11
|
-
|
12
|
-
# Sets the authorization scope for the current API key
|
13
|
-
def current_ability
|
14
|
-
@current_ability ||= Apicasso::Ability.new(@api_key)
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
# Identifies API key used in the request, avoiding unauthenticated access
|
20
|
-
def restrict_access
|
21
|
-
authenticate_or_request_with_http_token do |token, _options|
|
22
|
-
@api_key = Apicasso::Key.find_by!(token: token)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# Creates a request object in databse, registering the API key and
|
27
|
-
# a hash of the request and the response
|
28
|
-
def register_api_request
|
29
|
-
Apicasso::Request.delay.create(api_key_id: @api_key.try(:id),
|
30
|
-
object: { request: request_hash,
|
31
|
-
response: response_hash })
|
32
|
-
end
|
33
|
-
|
34
|
-
# Request data built as a hash.
|
35
|
-
# Returns UUID, URL, HTTP Headers and origin IP
|
36
|
-
def request_hash
|
37
|
-
{
|
38
|
-
uuid: request.uuid,
|
39
|
-
url: request.original_url,
|
40
|
-
headers: request.env.select { |key, _v| key =~ /^HTTP_/ },
|
41
|
-
ip: request.remote_ip
|
42
|
-
}
|
43
|
-
end
|
44
|
-
|
45
|
-
# Resonse data built as a hash.
|
46
|
-
# Returns HTTP Status and request body
|
47
|
-
def response_hash
|
48
|
-
{
|
49
|
-
status: response.status,
|
50
|
-
body: (response.body.present? ? JSON.parse(response.body) : '')
|
51
|
-
}
|
52
|
-
end
|
53
|
-
|
54
|
-
# Used to avoid errors parsing the search query,
|
55
|
-
# which can be passed as a JSON or as a key-value param
|
56
|
-
def parsed_query
|
57
|
-
JSON.parse(params[:q])
|
58
|
-
rescue JSON::ParserError, TypeError
|
59
|
-
params[:q]
|
60
|
-
end
|
61
|
-
|
62
|
-
# Used to avoid errors in included associations parsing
|
63
|
-
def parsed_include
|
64
|
-
params[:include].split(',')
|
65
|
-
rescue NoMethodError
|
66
|
-
[]
|
67
|
-
end
|
68
|
-
|
69
|
-
def parsed_select
|
70
|
-
params[:select].split(',')
|
71
|
-
rescue NoMethodError
|
72
|
-
[]
|
73
|
-
end
|
74
|
-
|
75
|
-
# Receives a `.paginate`d collection and returns the pagination
|
76
|
-
# metadata to be merged into response
|
77
|
-
def pagination_metadata_for(records)
|
78
|
-
{ total: records.total_entries,
|
79
|
-
total_pages: records.total_pages,
|
80
|
-
last_page: records.next_page.blank?,
|
81
|
-
previous_page: previous_link_for(records),
|
82
|
-
next_page: next_link_for(records),
|
83
|
-
out_of_bounds: records.out_of_bounds?,
|
84
|
-
offset: records.offset }
|
85
|
-
end
|
86
|
-
|
87
|
-
# Generates a contextualized URL of the next page for this request
|
88
|
-
def next_link_for(records)
|
89
|
-
uri = URI.parse(request.original_url)
|
90
|
-
query = Rack::Utils.parse_query(uri.query)
|
91
|
-
query['page'] = records.next_page
|
92
|
-
uri.query = Rack::Utils.build_query(query)
|
93
|
-
uri.to_s
|
94
|
-
end
|
95
|
-
|
96
|
-
# Generates a contextualized URL of the previous page for this request
|
97
|
-
def previous_link_for(records)
|
98
|
-
uri = URI.parse(request.original_url)
|
99
|
-
query = Rack::Utils.parse_query(uri.query)
|
100
|
-
query['page'] = records.previous_page
|
101
|
-
uri.query = Rack::Utils.build_query(query)
|
102
|
-
uri.to_s
|
103
|
-
end
|
104
|
-
|
105
|
-
# Receives a `:action, :resource, :object` hash to validate authorization
|
106
|
-
def authorize_for(opts = {})
|
107
|
-
authorize! opts[:action], opts[:resource] if opts[:resource].present?
|
108
|
-
authorize! opts[:action], opts[:object] if opts[:object].present?
|
109
|
-
end
|
110
|
-
|
111
|
-
def set_access_control_headers
|
112
|
-
response.headers['Access-Control-Allow-Origin'] = request.headers["Origin"]
|
113
|
-
response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
114
|
-
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
115
|
-
response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
|
116
|
-
response.headers['Access-Control-Max-Age'] = '1728000'
|
117
|
-
end
|
118
|
-
|
119
|
-
def preflight?
|
120
|
-
request.request_method == 'OPTIONS' &&
|
121
|
-
!request.headers['Authorization'].present?
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# Controller to extract common API features,
|
5
|
+
# such as authentication and authorization
|
6
|
+
class ApplicationController < ActionController::API
|
7
|
+
include ActionController::HttpAuthentication::Token::ControllerMethods
|
8
|
+
prepend_before_action :restrict_access, unless: -> { preflight? }
|
9
|
+
before_action :set_access_control_headers
|
10
|
+
after_action :register_api_request
|
11
|
+
|
12
|
+
# Sets the authorization scope for the current API key
|
13
|
+
def current_ability
|
14
|
+
@current_ability ||= Apicasso::Ability.new(@api_key)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Identifies API key used in the request, avoiding unauthenticated access
|
20
|
+
def restrict_access
|
21
|
+
authenticate_or_request_with_http_token do |token, _options|
|
22
|
+
@api_key = Apicasso::Key.find_by!(token: token)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates a request object in databse, registering the API key and
|
27
|
+
# a hash of the request and the response
|
28
|
+
def register_api_request
|
29
|
+
Apicasso::Request.delay.create(api_key_id: @api_key.try(:id),
|
30
|
+
object: { request: request_hash,
|
31
|
+
response: response_hash })
|
32
|
+
end
|
33
|
+
|
34
|
+
# Request data built as a hash.
|
35
|
+
# Returns UUID, URL, HTTP Headers and origin IP
|
36
|
+
def request_hash
|
37
|
+
{
|
38
|
+
uuid: request.uuid,
|
39
|
+
url: request.original_url,
|
40
|
+
headers: request.env.select { |key, _v| key =~ /^HTTP_/ },
|
41
|
+
ip: request.remote_ip
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Resonse data built as a hash.
|
46
|
+
# Returns HTTP Status and request body
|
47
|
+
def response_hash
|
48
|
+
{
|
49
|
+
status: response.status,
|
50
|
+
body: (response.body.present? ? JSON.parse(response.body) : '')
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Used to avoid errors parsing the search query,
|
55
|
+
# which can be passed as a JSON or as a key-value param
|
56
|
+
def parsed_query
|
57
|
+
JSON.parse(params[:q])
|
58
|
+
rescue JSON::ParserError, TypeError
|
59
|
+
params[:q]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Used to avoid errors in included associations parsing
|
63
|
+
def parsed_include
|
64
|
+
params[:include].split(',')
|
65
|
+
rescue NoMethodError
|
66
|
+
[]
|
67
|
+
end
|
68
|
+
|
69
|
+
def parsed_select
|
70
|
+
params[:select].split(',')
|
71
|
+
rescue NoMethodError
|
72
|
+
[]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Receives a `.paginate`d collection and returns the pagination
|
76
|
+
# metadata to be merged into response
|
77
|
+
def pagination_metadata_for(records)
|
78
|
+
{ total: records.total_entries,
|
79
|
+
total_pages: records.total_pages,
|
80
|
+
last_page: records.next_page.blank?,
|
81
|
+
previous_page: previous_link_for(records),
|
82
|
+
next_page: next_link_for(records),
|
83
|
+
out_of_bounds: records.out_of_bounds?,
|
84
|
+
offset: records.offset }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Generates a contextualized URL of the next page for this request
|
88
|
+
def next_link_for(records)
|
89
|
+
uri = URI.parse(request.original_url)
|
90
|
+
query = Rack::Utils.parse_query(uri.query)
|
91
|
+
query['page'] = records.next_page
|
92
|
+
uri.query = Rack::Utils.build_query(query)
|
93
|
+
uri.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
# Generates a contextualized URL of the previous page for this request
|
97
|
+
def previous_link_for(records)
|
98
|
+
uri = URI.parse(request.original_url)
|
99
|
+
query = Rack::Utils.parse_query(uri.query)
|
100
|
+
query['page'] = records.previous_page
|
101
|
+
uri.query = Rack::Utils.build_query(query)
|
102
|
+
uri.to_s
|
103
|
+
end
|
104
|
+
|
105
|
+
# Receives a `:action, :resource, :object` hash to validate authorization
|
106
|
+
def authorize_for(opts = {})
|
107
|
+
authorize! opts[:action], opts[:resource] if opts[:resource].present?
|
108
|
+
authorize! opts[:action], opts[:object] if opts[:object].present?
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_access_control_headers
|
112
|
+
response.headers['Access-Control-Allow-Origin'] = request.headers["Origin"]
|
113
|
+
response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
114
|
+
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
115
|
+
response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
|
116
|
+
response.headers['Access-Control-Max-Age'] = '1728000'
|
117
|
+
end
|
118
|
+
|
119
|
+
def preflight?
|
120
|
+
request.request_method == 'OPTIONS' &&
|
121
|
+
!request.headers['Authorization'].present?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -1,211 +1,211 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
# Controller to consume read-only data to be used on client's frontend
|
5
|
-
class CrudController < Apicasso::ApplicationController
|
6
|
-
before_action :set_root_resource
|
7
|
-
before_action :set_object, except: %i[index schema create]
|
8
|
-
before_action :set_nested_resource, only: %i[nested_index]
|
9
|
-
before_action :set_records, only: %i[index nested_index]
|
10
|
-
|
11
|
-
include Orderable
|
12
|
-
|
13
|
-
# GET /:resource
|
14
|
-
# Returns a paginated, ordered and filtered query based response.
|
15
|
-
# Consider this
|
16
|
-
# To get all `Channel` sorted by ascending `name` and descending
|
17
|
-
# `updated_at`, filtered by the ones that have a `domain` that matches
|
18
|
-
# exactly `"domain.com"`, paginating records 42 per page and retrieving
|
19
|
-
# the page 42 of that collection. Usage:
|
20
|
-
# GET /sites?sort=+name,-updated_at&q[domain_eq]=domain.com&page=42&per_page=42
|
21
|
-
def index
|
22
|
-
set_access_control_headers
|
23
|
-
render json: index_json
|
24
|
-
end
|
25
|
-
|
26
|
-
# GET /:resource/1
|
27
|
-
# Common behavior for showing a record, with an addition of
|
28
|
-
# relation/methods including on response
|
29
|
-
def show
|
30
|
-
set_access_control_headers
|
31
|
-
render json: show_json
|
32
|
-
end
|
33
|
-
|
34
|
-
# PATCH/PUT /:resource/1
|
35
|
-
# Common behavior for an update API endpoint
|
36
|
-
def update
|
37
|
-
authorize_for(action: :update,
|
38
|
-
resource: resource.name.underscore.to_sym,
|
39
|
-
object: @object)
|
40
|
-
if @object.update(object_params)
|
41
|
-
render json: @object
|
42
|
-
else
|
43
|
-
render json: @object.errors, status: :unprocessable_entity
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# DELETE /:resource/1
|
48
|
-
# Common behavior for an destroy API endpoint
|
49
|
-
def destroy
|
50
|
-
authorize_for(action: :destroy,
|
51
|
-
resource: resource.name.underscore.to_sym,
|
52
|
-
object: @object)
|
53
|
-
if @object.destroy
|
54
|
-
head :no_content, status: :ok
|
55
|
-
else
|
56
|
-
render json: @object.errors, status: :unprocessable_entity
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# GET /:resource/1/:nested_resource
|
61
|
-
alias nested_index index
|
62
|
-
|
63
|
-
# POST /:resource
|
64
|
-
def create
|
65
|
-
@object = resource.new(resource_params)
|
66
|
-
authorize_for(action: :create,
|
67
|
-
resource: resource.name.underscore.to_sym,
|
68
|
-
object: @object)
|
69
|
-
if @object.save
|
70
|
-
render json: @object, status: :created, location: @object
|
71
|
-
else
|
72
|
-
render json: @object.errors, status: :unprocessable_entity
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# OPTIONS /:resource
|
77
|
-
# OPTIONS /:resource/1/:nested_resource
|
78
|
-
# Will return a JSON with the schema of the current resource, using
|
79
|
-
# attribute names as keys and attirbute types as values.
|
80
|
-
def schema
|
81
|
-
render json: resource_schema.to_json unless preflight?
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
# Common setup to stablish which model is the resource of this request
|
87
|
-
def set_root_resource
|
88
|
-
@root_resource = params[:resource].classify.constantize
|
89
|
-
end
|
90
|
-
|
91
|
-
# Common setup to stablish which object this request is querying
|
92
|
-
def set_object
|
93
|
-
id = params[:id]
|
94
|
-
@object = resource.friendly.find(id)
|
95
|
-
rescue NoMethodError
|
96
|
-
@object = resource.find(id)
|
97
|
-
ensure
|
98
|
-
authorize! :read, @object
|
99
|
-
end
|
100
|
-
|
101
|
-
# Setup to stablish the nested model to be queried
|
102
|
-
def set_nested_resource
|
103
|
-
@nested_resource = @object.send(params[:nested].underscore.pluralize)
|
104
|
-
end
|
105
|
-
|
106
|
-
# Reutrns root_resource if nested_resource is not set scoped by permissions
|
107
|
-
def resource
|
108
|
-
(@nested_resource || @root_resource)
|
109
|
-
end
|
110
|
-
|
111
|
-
# Used to setup the resource's schema, mapping attributes and it's types
|
112
|
-
def resource_schema
|
113
|
-
schemated = {}
|
114
|
-
resource.columns_hash.each { |key, value| schemated[key] = value.type }
|
115
|
-
schemated
|
116
|
-
end
|
117
|
-
|
118
|
-
# Used to setup the records from the selected resource that are
|
119
|
-
# going to be rendered, if authorized
|
120
|
-
def set_records
|
121
|
-
authorize! :read, resource.name.underscore.to_sym
|
122
|
-
@records = resource.ransack(parsed_query).result
|
123
|
-
key_scope_records
|
124
|
-
reorder_records if params[:sort].present?
|
125
|
-
select_fields if params[:select].present?
|
126
|
-
include_relations if params[:include].present?
|
127
|
-
end
|
128
|
-
|
129
|
-
# Selects a fieldset that should be returned, instead of all fields
|
130
|
-
# from records.
|
131
|
-
def select_fields
|
132
|
-
@records = @records.select(*params[:select].split(','))
|
133
|
-
end
|
134
|
-
|
135
|
-
# Reordering of records which happens when receiving `params[:sort]`
|
136
|
-
def reorder_records
|
137
|
-
@records = @records.unscope(:order).order(ordering_params(params))
|
138
|
-
end
|
139
|
-
|
140
|
-
# Raw paginated records object
|
141
|
-
def paginated_records
|
142
|
-
@records
|
143
|
-
.paginate(page: params[:page], per_page: params[:per_page])
|
144
|
-
end
|
145
|
-
|
146
|
-
# Records that can be accessed from current Apicasso::Key scope
|
147
|
-
# permissions
|
148
|
-
def key_scope_records
|
149
|
-
@records = @records.accessible_by(current_ability).unscope(:order)
|
150
|
-
end
|
151
|
-
|
152
|
-
# The response for index action, which can be a pagination of a record collection
|
153
|
-
# or a grouped count of attributes
|
154
|
-
def index_json
|
155
|
-
if params[:group].present?
|
156
|
-
@records.group(params[:group][:by].split(',')).send(params[:group][:calculate], params[:group][:fields])
|
157
|
-
else
|
158
|
-
collection_response
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# The response for show action, which can be a fieldset
|
163
|
-
# or a full response of attributes
|
164
|
-
def show_json
|
165
|
-
if params[:select].present?
|
166
|
-
@object.to_json(include: parsed_include, only: parsed_select)
|
167
|
-
else
|
168
|
-
@object.to_json(include: parsed_include)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Parsing of `paginated_records` with pagination variables metadata
|
173
|
-
def built_paginated
|
174
|
-
{ entries: paginated_records }.merge(pagination_metadata_for(paginated_records))
|
175
|
-
end
|
176
|
-
|
177
|
-
# All records matching current query and it's total
|
178
|
-
def built_unpaginated
|
179
|
-
{ entries: @records, total: @records.size }
|
180
|
-
end
|
181
|
-
|
182
|
-
# Parsed JSON to be used as response payload, with included relations
|
183
|
-
def include_relations
|
184
|
-
@records = JSON.parse(included_collection.to_json(include: parsed_include))
|
185
|
-
rescue ActiveRecord::AssociationNotFoundError
|
186
|
-
@records = JSON.parse(@records.to_json(include: parsed_include))
|
187
|
-
end
|
188
|
-
|
189
|
-
# A way to SQL-include for current param[:include], only if available
|
190
|
-
def included_collection
|
191
|
-
@records.includes(parsed_include)
|
192
|
-
rescue ActiveRecord::AssociationNotFoundError
|
193
|
-
@records
|
194
|
-
end
|
195
|
-
|
196
|
-
# Returns the collection checking if it needs pagination
|
197
|
-
def collection_response
|
198
|
-
if params[:per_page].to_i < 0
|
199
|
-
built_unpaginated
|
200
|
-
else
|
201
|
-
built_paginated
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
# Only allow a trusted parameter "white list" through,
|
206
|
-
# based on resource's schema.
|
207
|
-
def object_params
|
208
|
-
params.fetch(resource.name.underscore.to_sym, resource_schema.keys)
|
209
|
-
end
|
210
|
-
end
|
211
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# Controller to consume read-only data to be used on client's frontend
|
5
|
+
class CrudController < Apicasso::ApplicationController
|
6
|
+
before_action :set_root_resource
|
7
|
+
before_action :set_object, except: %i[index schema create]
|
8
|
+
before_action :set_nested_resource, only: %i[nested_index]
|
9
|
+
before_action :set_records, only: %i[index nested_index]
|
10
|
+
|
11
|
+
include Orderable
|
12
|
+
|
13
|
+
# GET /:resource
|
14
|
+
# Returns a paginated, ordered and filtered query based response.
|
15
|
+
# Consider this
|
16
|
+
# To get all `Channel` sorted by ascending `name` and descending
|
17
|
+
# `updated_at`, filtered by the ones that have a `domain` that matches
|
18
|
+
# exactly `"domain.com"`, paginating records 42 per page and retrieving
|
19
|
+
# the page 42 of that collection. Usage:
|
20
|
+
# GET /sites?sort=+name,-updated_at&q[domain_eq]=domain.com&page=42&per_page=42
|
21
|
+
def index
|
22
|
+
set_access_control_headers
|
23
|
+
render json: index_json
|
24
|
+
end
|
25
|
+
|
26
|
+
# GET /:resource/1
|
27
|
+
# Common behavior for showing a record, with an addition of
|
28
|
+
# relation/methods including on response
|
29
|
+
def show
|
30
|
+
set_access_control_headers
|
31
|
+
render json: show_json
|
32
|
+
end
|
33
|
+
|
34
|
+
# PATCH/PUT /:resource/1
|
35
|
+
# Common behavior for an update API endpoint
|
36
|
+
def update
|
37
|
+
authorize_for(action: :update,
|
38
|
+
resource: resource.name.underscore.to_sym,
|
39
|
+
object: @object)
|
40
|
+
if @object.update(object_params)
|
41
|
+
render json: @object
|
42
|
+
else
|
43
|
+
render json: @object.errors, status: :unprocessable_entity
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# DELETE /:resource/1
|
48
|
+
# Common behavior for an destroy API endpoint
|
49
|
+
def destroy
|
50
|
+
authorize_for(action: :destroy,
|
51
|
+
resource: resource.name.underscore.to_sym,
|
52
|
+
object: @object)
|
53
|
+
if @object.destroy
|
54
|
+
head :no_content, status: :ok
|
55
|
+
else
|
56
|
+
render json: @object.errors, status: :unprocessable_entity
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# GET /:resource/1/:nested_resource
|
61
|
+
alias nested_index index
|
62
|
+
|
63
|
+
# POST /:resource
|
64
|
+
def create
|
65
|
+
@object = resource.new(resource_params)
|
66
|
+
authorize_for(action: :create,
|
67
|
+
resource: resource.name.underscore.to_sym,
|
68
|
+
object: @object)
|
69
|
+
if @object.save
|
70
|
+
render json: @object, status: :created, location: @object
|
71
|
+
else
|
72
|
+
render json: @object.errors, status: :unprocessable_entity
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# OPTIONS /:resource
|
77
|
+
# OPTIONS /:resource/1/:nested_resource
|
78
|
+
# Will return a JSON with the schema of the current resource, using
|
79
|
+
# attribute names as keys and attirbute types as values.
|
80
|
+
def schema
|
81
|
+
render json: resource_schema.to_json unless preflight?
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Common setup to stablish which model is the resource of this request
|
87
|
+
def set_root_resource
|
88
|
+
@root_resource = params[:resource].classify.constantize
|
89
|
+
end
|
90
|
+
|
91
|
+
# Common setup to stablish which object this request is querying
|
92
|
+
def set_object
|
93
|
+
id = params[:id]
|
94
|
+
@object = resource.friendly.find(id)
|
95
|
+
rescue NoMethodError
|
96
|
+
@object = resource.find(id)
|
97
|
+
ensure
|
98
|
+
authorize! :read, @object
|
99
|
+
end
|
100
|
+
|
101
|
+
# Setup to stablish the nested model to be queried
|
102
|
+
def set_nested_resource
|
103
|
+
@nested_resource = @object.send(params[:nested].underscore.pluralize)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Reutrns root_resource if nested_resource is not set scoped by permissions
|
107
|
+
def resource
|
108
|
+
(@nested_resource || @root_resource)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Used to setup the resource's schema, mapping attributes and it's types
|
112
|
+
def resource_schema
|
113
|
+
schemated = {}
|
114
|
+
resource.columns_hash.each { |key, value| schemated[key] = value.type }
|
115
|
+
schemated
|
116
|
+
end
|
117
|
+
|
118
|
+
# Used to setup the records from the selected resource that are
|
119
|
+
# going to be rendered, if authorized
|
120
|
+
def set_records
|
121
|
+
authorize! :read, resource.name.underscore.to_sym
|
122
|
+
@records = resource.ransack(parsed_query).result
|
123
|
+
key_scope_records
|
124
|
+
reorder_records if params[:sort].present?
|
125
|
+
select_fields if params[:select].present?
|
126
|
+
include_relations if params[:include].present?
|
127
|
+
end
|
128
|
+
|
129
|
+
# Selects a fieldset that should be returned, instead of all fields
|
130
|
+
# from records.
|
131
|
+
def select_fields
|
132
|
+
@records = @records.select(*params[:select].split(','))
|
133
|
+
end
|
134
|
+
|
135
|
+
# Reordering of records which happens when receiving `params[:sort]`
|
136
|
+
def reorder_records
|
137
|
+
@records = @records.unscope(:order).order(ordering_params(params))
|
138
|
+
end
|
139
|
+
|
140
|
+
# Raw paginated records object
|
141
|
+
def paginated_records
|
142
|
+
@records
|
143
|
+
.paginate(page: params[:page], per_page: params[:per_page])
|
144
|
+
end
|
145
|
+
|
146
|
+
# Records that can be accessed from current Apicasso::Key scope
|
147
|
+
# permissions
|
148
|
+
def key_scope_records
|
149
|
+
@records = @records.accessible_by(current_ability).unscope(:order)
|
150
|
+
end
|
151
|
+
|
152
|
+
# The response for index action, which can be a pagination of a record collection
|
153
|
+
# or a grouped count of attributes
|
154
|
+
def index_json
|
155
|
+
if params[:group].present?
|
156
|
+
@records.group(params[:group][:by].split(',')).send(params[:group][:calculate], params[:group][:fields])
|
157
|
+
else
|
158
|
+
collection_response
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# The response for show action, which can be a fieldset
|
163
|
+
# or a full response of attributes
|
164
|
+
def show_json
|
165
|
+
if params[:select].present?
|
166
|
+
@object.to_json(include: parsed_include, only: parsed_select)
|
167
|
+
else
|
168
|
+
@object.to_json(include: parsed_include)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Parsing of `paginated_records` with pagination variables metadata
|
173
|
+
def built_paginated
|
174
|
+
{ entries: paginated_records }.merge(pagination_metadata_for(paginated_records))
|
175
|
+
end
|
176
|
+
|
177
|
+
# All records matching current query and it's total
|
178
|
+
def built_unpaginated
|
179
|
+
{ entries: @records, total: @records.size }
|
180
|
+
end
|
181
|
+
|
182
|
+
# Parsed JSON to be used as response payload, with included relations
|
183
|
+
def include_relations
|
184
|
+
@records = JSON.parse(included_collection.to_json(include: parsed_include))
|
185
|
+
rescue ActiveRecord::AssociationNotFoundError
|
186
|
+
@records = JSON.parse(@records.to_json(include: parsed_include))
|
187
|
+
end
|
188
|
+
|
189
|
+
# A way to SQL-include for current param[:include], only if available
|
190
|
+
def included_collection
|
191
|
+
@records.includes(parsed_include)
|
192
|
+
rescue ActiveRecord::AssociationNotFoundError
|
193
|
+
@records
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns the collection checking if it needs pagination
|
197
|
+
def collection_response
|
198
|
+
if params[:per_page].to_i < 0
|
199
|
+
built_unpaginated
|
200
|
+
else
|
201
|
+
built_paginated
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Only allow a trusted parameter "white list" through,
|
206
|
+
# based on resource's schema.
|
207
|
+
def object_params
|
208
|
+
params.fetch(resource.name.underscore.to_sym, resource_schema.keys)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|