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