apicasso 0.2.16 → 0.3.0

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