apicasso 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9eab0692116c8e9b6d81f00ae6012ac0125d06e
4
- data.tar.gz: d9fbca376a48478b49055e43b5c0b2a51e59893f
3
+ metadata.gz: ab5b6196d2e1a9675b5b4eca26522f6ff1dabe63
4
+ data.tar.gz: 1715dc2d29b09e97deba8f41b811709059a3520c
5
5
  SHA512:
6
- metadata.gz: aed068877154de286f451f0f41d7049d79421af273873ade2544329287cd8727b2e949415cebdcff6d354b76965cdfb205ad2983845cbecee686f7786541437c
7
- data.tar.gz: fa8bc7de5e9000216473ceeddc07d4a9ad89ed1d94a470507416a41d8ef1880c728570d224ee09c9e2a695600fa6d559faf17e92c6be50d40eb4288983d8070c
6
+ metadata.gz: f9a542fe03655f97043183cb6040d0ff92a0d533a337faf43a4a4771cc0ba75e2ce73c2494d98b0c8436b049bcf548ae0e14cdd8ba26ae0a1b42be2f3186f502
7
+ data.tar.gz: 5ff7a98726754f89f1cf6f50038b2f5c062f49d204626b5dda58da7eb85d08643ddbc0aa0d9c057f5241a3cb47044f6146431be6453159f933464b47c6b49892
data/README.md CHANGED
@@ -6,6 +6,7 @@ JSON API development can get boring and time consuming. If you think it through,
6
6
  It is a route-based resource abstraction using API key scoping. This makes it possible to make CRUD-only applications just by creating functional Rails' models. It is a perfect candidate for legacy Rails projects that do not have an API. Access to your application's resources is managed by a `.scope` JSON object per API key. It uses that permission scope to restrict and extend access.
7
7
 
8
8
  # Installation
9
+
9
10
  Add this line to your application's `Gemfile`:
10
11
 
11
12
  ```ruby
@@ -13,9 +14,11 @@ gem 'apicasso'
13
14
  ```
14
15
 
15
16
  And then execute this to generate the required migrations:
17
+
16
18
  ```bash
17
19
  $ bundle install && rails g apicasso:install
18
20
  ```
21
+
19
22
  You will need to use a database with JSON fields support to use this gem.
20
23
 
21
24
  # Usage
@@ -25,6 +28,7 @@ You will need to use a database with JSON fields support to use this gem.
25
28
  ## Mounting engine into `config/routes.rb`
26
29
 
27
30
  After installing it, you can mount a full-fledged CRUD JSON API just by attaching into some route. Usually you will have it under a scoped route like `/api/v1` or a subdomain. You can do that by adding this into your `config/routes.rb`:
31
+
28
32
  ```ruby
29
33
  # To mount your APIcasso routes under the path scope `/api/v1`
30
34
  mount Apicasso::Engine, at: "/api/v1"
@@ -33,7 +37,9 @@ After installing it, you can mount a full-fledged CRUD JSON API just by attachin
33
37
  mount Apicasso::Engine, at: "/"
34
38
  end
35
39
  ```
40
+
36
41
  Your API will reflect very similarly a `resources :resource` statement with the following routes:
42
+
37
43
  ```ruby
38
44
  get '/:resource/' # Index action, listing a `:resource` collection from your application
39
45
  post '/:resource/' # Create action for one `:resource` from your application
@@ -50,11 +56,14 @@ This means all your application's models will be exposed as `:resource` and it's
50
56
  ## Extending base API actions
51
57
 
52
58
  When your application needs some kind of custom interaction that is not covered by APIcasso's CRUD approach you can make your own actions using our base classes and objects to go straight into your logic. If you have built the APIcasso's engine into a route it is important that your custom action takes precedence over the gem's ones. To do that you need to declare your custom route before the engine on you `config/routes.rb`
59
+
53
60
  ```ruby
54
61
  match '/:resource/:id/a-custom-action' => 'custom#not_a_crud', via: :get
55
62
  mount Apicasso::Engine, at: "/api/v1"
56
63
  ```
64
+
57
65
  And in your `app/controllers/custom_controller.rb` you would have something like:
66
+
58
67
  ```ruby
59
68
  class CustomController < Apicasso::CrudController
60
69
  def not_a_crud
@@ -62,17 +71,27 @@ And in your `app/controllers/custom_controller.rb` you would have something like
62
71
  end
63
72
  end
64
73
  ```
74
+
65
75
  This way you enjoy all our object finder, authorization and authentication features, making your job more straight into your business logic.
66
76
 
67
- ## Authorization/Authentication
77
+ ## Authentication
68
78
 
69
- > But exposing my models to the internet is permissive as hell! Haven't you thought about security?
79
+ > But exposing my models to the internet is permissive as hell! Haven't you thought about security?
70
80
 
71
- *Sure!* The **APIcasso** suite is exposing your application using authentication through `Authorization: Token` [HTTP header authentication](http://tools.ietf.org/html/draft-hammer-http-token-auth-01). The API key objects are manageable through the `Apicasso::Key` model, which gets setup at install. When a new key is created a `.token` is generated using an [Universally Unique Identifier(RFC 4122)](https://tools.ietf.org/html/rfc4122).
81
+ _Sure!_ The **APIcasso** suite is exposing your application using authentication through `Authorization: Token` [HTTP header authentication](http://tools.ietf.org/html/draft-hammer-http-token-auth-01). The API key objects are manageable through the `Apicasso::Key` model, which gets setup at install. When a new key is created a `.token` is generated using an [Universally Unique Identifier(RFC 4122)](https://tools.ietf.org/html/rfc4122). A authenticated request looks like this:
72
82
 
73
- Each `Apicasso::Key` object has a token attribute, which is used to define the authorized access identification.
83
+ ```
84
+ curl -X GET \
85
+ https://apixample.com/v1/your_app_resource \
86
+ -H 'authorization: Token token=cda4e9f633c123ef9ddce5e6564292b3'
87
+ ```
88
+
89
+ Each `Apicasso::Key` object has a token attribute, which is used on this header to authorize access. For now, there is no plans for a login/JWT logic, you should implement this in your project's scope.
90
+
91
+ ## Authorization
74
92
 
75
93
  Your Models are then exposed based on each `Apicasso::Key.scope` definition, which is a way to configure how much of your application each key can access. I.E.:
94
+
76
95
  ```ruby
77
96
  Apicasso::Key.create(scope:
78
97
  { manage:
@@ -81,13 +100,15 @@ Your Models are then exposed based on each `Apicasso::Key.scope` definition, whi
81
100
  { account: { manager_id: 1 } }
82
101
  })
83
102
  ```
103
+
84
104
  > The key from this example will have full access to all orders and to users with `account_id == 1`. It will have also read-only access to accounts with `id == 1`.
85
105
 
86
- A scope configured like this translates directly into which kind of access each key has on all of your application's models. This kind of authorization is why one of the dependencies for this gem is [CanCanCan](https://github.com/CanCanCommunity/cancancan), which abstracts the scope field into your API access control.
106
+ A scope configured like this translates directly into which kind of access each key has on all of your application's models. This kind of authorization is why one of the dependencies for this gem is [CanCanCan](https://github.com/CanCanCommunity/cancancan), which abstracts the scope field into your API access control.
87
107
 
88
108
  You can have two kind of access control:
89
- * `true` - This will mean the key will have the declared clearance on **ALL** of this model's records
90
- * `Hash` - This will build a condition to what records this key have access to. A scope as `{ read: [{ account: { manager_id: 1 } }] }` will have read access into accounts with `manager_id == 1`
109
+
110
+ - `true` - This will mean the key will have the declared clearance on **ALL** of this model's records
111
+ - `Hash` - This will build a condition to what records this key have access to. A scope as `{ read: [{ account: { manager_id: 1 } }] }` will have read access into accounts with `manager_id == 1`
91
112
 
92
113
  This saves you the trouble of having to setup every controller for each model. And even if your application really needs it, just make your controllers inherit from `Apicasso::CrudController` extending it and enabling the use of `@object` and `@resource` variables to access what is being resquested.
93
114
 
@@ -100,6 +121,7 @@ The index actions present in the gem are already equipped with pagination, order
100
121
  You can sort a collection query by using a URL parameter with field names preffixed with `+` or `-` to configure custom ordering per request.
101
122
 
102
123
  To order a collection with ascending `updated_at` and descending `name` you can add the `sort` parameter with those fields as options, indicating which kind of ordination you want to give to each one:
124
+
103
125
  ```
104
126
  ?sort=+updated_at,-name
105
127
  ```
@@ -107,18 +129,23 @@ To order a collection with ascending `updated_at` and descending `name` you can
107
129
  ### Filtering/Search
108
130
 
109
131
  APIcasso has [ransack's search matchers](https://github.com/activerecord-hackery/ransack#search-matchers) on it's index actions. This means you can dynamically build search queries with any of your resource's fields, this will be done by using a `?q` parameter which groups all your filtering options on your requests. If you wanted to search all your records and return only the ones with `full_name` starting with `Picasso` your query would look something like this:
132
+
110
133
  ```
111
134
  ?q[full_name_start]=Picasso
112
135
  ```
136
+
113
137
  To build complex search queries you can chain many parameter options or check [ransack's wiki](https://github.com/activerecord-hackery/ransack/wiki/) on how to adapt this feature into your project's needs.
114
138
 
115
139
  ### Pagination
116
140
 
117
141
  Automatic pagination is done in index actions, with the adittion of some metadata to help on the data consumption. You can pass page and per page parameters to build pagination options into your needs. And on requests that you need unpaginated collections, just pass a lower than zero `per_page`. Example of a pagination query string:
142
+
118
143
  ```
119
144
  ?page=2&per_page=12
120
145
  ```
146
+
121
147
  Your colletion will be build inside a JSON along with some metadata about it. The response structure is:
148
+
122
149
  ```
123
150
  { entries: [{Record1}, {Record2}, {Record3} ... {Record12}],
124
151
  total: 1234,
@@ -134,43 +161,62 @@ Your colletion will be build inside a JSON along with some metadata about it. Th
134
161
 
135
162
  Sometimes your data can grow large in some tables and you need to consumed only a limited set of data on a given frontend application. To avoid large requests and filtering a lot of unused data with JS you can restrict which fields you need on your API's reponse. This is done adding a `?select` parameter. Just pass the field names you desire splitted by `,`
136
163
  Let's say you are building a user list with their name, e-mails and phones, to get only those fields your URL query would look something like:
164
+
137
165
  ```
138
166
  ?select=name,email,phone
139
167
  ```
140
- This will change the response to filter out the unwanted attributes.
168
+
169
+ This will change the response to return only the requested attributes. You need to observe that your business logic may require some fields for a valid response to be returned. **This method can be used both on index and show actions**
170
+
171
+ ### Including relations or methods on response
172
+
173
+ If there is any method or relation that you want to be inserted on the payload, you just need to pass them as a part of the URL query like this:
174
+
175
+ ```
176
+ ?include=pictures,suggestions
177
+ ```
178
+
179
+ This will insert the contents of `.pictures` and `.suggestions` on the payload, along with the records' data. **This method can be used both on index and show actions**
141
180
 
142
181
  ### Grouping operations
143
182
 
144
183
  If you need to make grouping calculations, like:
145
- * Counting of all records, or by one **optional** field presence
146
- * Maximum value of one field
147
- * Minimum value of one field
148
- * Average value of one field
149
- * Value sum of one field
184
+
185
+ - Counting of all records, or by one **optional** field presence
186
+ - Maximum value of one field
187
+ - Minimum value of one field
188
+ - Average value of one field
189
+ - Value sum of one field
150
190
 
151
191
  Grouping is done by the combination of 3 parameters
192
+
152
193
  ```
153
194
  ?group[by]=a_field&group[calculate]=count&group[fields]=another_field
154
195
  ```
196
+
155
197
  Each of those attributes on the `?group` parameter represent an option of the query being made.
156
- - `group[by]` - Represents which field will be the key for the grouping behavior
157
- - `group[calculate]` - Which calculation will be sent in the response. Options are: `count`, `maximum`, `minimum`, `average`, `sum`
158
- - `group[fields]` - Represents which field will be the base for the response calculation.
198
+
199
+ - `group[by]` - Represents which field will be the key for the grouping behavior
200
+ - `group[calculate]` - Which calculation will be sent in the response. Options are: `count`, `maximum`, `minimum`, `average`, `sum`
201
+ - `group[fields]` - Represents which field will be the base for the response calculation.
159
202
 
160
203
  # Contributing
204
+
161
205
  Bug reports and pull requests are welcome on GitHub at https://github.com/ErvalhouS/APIcasso. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant code of conduct](http://contributor-covenant.org/). To find good places to start contributing, try looking into our issue list and our Codeclimate profile, or if you want to participate actively on what the core team is working on checkout our todo list:
162
206
 
163
207
  ### TODO
164
208
 
165
- - Abstract a configurable CORS approach, maybe using middleware.
166
- - Add gem options like: Token rotation, Alternative authentication methods
167
- - Add latest auto-documentation feature into README
168
- - Rate limiting
169
- - Testing suite
170
- - Travis CI
209
+ - Abstract a configurable CORS approach, maybe using middleware.
210
+ - Add gem options like: Token rotation, Alternative authentication methods
211
+ - Add latest auto-documentation feature into README
212
+ - Rate limiting
213
+ - Testing suite
214
+ - Travis CI
171
215
 
172
216
  # Code of conduct
217
+
173
218
  Everyone interacting in the APIcasso project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ErvalhouS/APIcasso/blob/master/CODE_OF_CONDUCT.md).
174
219
 
175
220
  # License
221
+
176
222
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,39 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apicasso
4
- # Controller to extract common API features,
5
- # such as authentication and authorization
4
+ # Controller to extract common API features, such as authentication and
5
+ # authorization. Used to be inherited by non-CRUD controllers when your
6
+ # application needs to create custom actions.
6
7
  class ApplicationController < ActionController::API
7
8
  include ActionController::HttpAuthentication::Token::ControllerMethods
8
9
  prepend_before_action :restrict_access, unless: -> { preflight? }
9
10
  before_action :set_access_control_headers
10
11
  after_action :register_api_request
11
12
 
12
- # Sets the authorization scope for the current API key
13
+ # Sets the authorization scope for the current API key, it's a getter
14
+ # to make scoping easier
13
15
  def current_ability
14
16
  @current_ability ||= Apicasso::Ability.new(@api_key)
15
17
  end
16
18
 
17
19
  private
18
20
 
19
- # Identifies API key used in the request, avoiding unauthenticated access
21
+ # Identifies API key used in the request, avoiding unauthenticated access.
22
+ # Responds with status 401 when token is not present or not found.
23
+ # Access restriction happens on the `Authentication` HTTP header.
24
+ # Example:
25
+ # curl -X GET http://example.com/objects -H 'authorization: Token token=f1e048a0b0ef4071a9a64ceecd48c64b'
20
26
  def restrict_access
21
27
  authenticate_or_request_with_http_token do |token, _options|
22
- @api_key = Apicasso::Key.find_by!(token: token)
28
+ @api_key = Apicasso::Key.find_by(token: token)
23
29
  end
24
30
  end
25
31
 
26
32
  # Creates a request object in databse, registering the API key and
27
- # a hash of the request and the response
33
+ # a hash of the request and the response. It's an auditing proccess,
34
+ # all relevant information about the requests and it's reponses get
35
+ # recorded within the `Apicasso::Request`. This method assumes that
36
+ # your project is using some kind of ActiveRecord extension with a
37
+ # `.delay` method, which when not present makes your API very slow.
28
38
  def register_api_request
29
39
  Apicasso::Request.delay.create(api_key_id: @api_key.try(:id),
30
- object: { request: request_hash,
31
- response: response_hash })
40
+ object: { request: request_metadata,
41
+ response: response_metadata })
42
+ rescue NoMethodError
43
+ Apicasso::Request.create(api_key_id: @api_key.try(:id),
44
+ object: { request: request_metadata,
45
+ response: response_metadata })
32
46
  end
33
47
 
34
- # Request data built as a hash.
35
- # Returns UUID, URL, HTTP Headers and origin IP
36
- def request_hash
48
+ # Information that gets inserted on `register_api_request` as auditing data
49
+ # about the request. Returns a Hash with UUID, URL, HTTP Headers and IP
50
+ def request_metadata
37
51
  {
38
52
  uuid: request.uuid,
39
53
  url: request.original_url,
@@ -42,30 +56,34 @@ module Apicasso
42
56
  }
43
57
  end
44
58
 
45
- # Resonse data built as a hash.
46
- # Returns HTTP Status and request body
47
- def response_hash
59
+ # Information that gets inserted on `register_api_request` as auditing data
60
+ # about the response sent back to the client. Returns HTTP Status and request body
61
+ def response_metadata
48
62
  {
49
63
  status: response.status,
50
64
  body: (response.body.present? ? JSON.parse(response.body) : '')
51
65
  }
52
66
  end
53
67
 
54
- # Used to avoid errors parsing the search query,
55
- # which can be passed as a JSON or as a key-value param
68
+ # Used to avoid errors parsing the search query, which can be passed as
69
+ # a JSON or as a key-value param. JSON is preferred because it generates
70
+ # shorter URLs on GET parameters.
56
71
  def parsed_query
57
72
  JSON.parse(params[:q])
58
73
  rescue JSON::ParserError, TypeError
59
74
  params[:q]
60
75
  end
61
76
 
62
- # Used to avoid errors in included associations parsing
77
+ # Used to avoid errors in included associations parsing and to enable a
78
+ # insertion point for a change on splitting method.
63
79
  def parsed_include
64
80
  params[:include].split(',')
65
81
  rescue NoMethodError
66
82
  []
67
83
  end
68
84
 
85
+ # Used to avoid errors in fieldset selection parsing and to enable a
86
+ # insertion point for a change on splitting method.
69
87
  def parsed_select
70
88
  params[:select].split(',')
71
89
  rescue NoMethodError
@@ -84,7 +102,7 @@ module Apicasso
84
102
  offset: records.offset }
85
103
  end
86
104
 
87
- # Generates a contextualized URL of the next page for this request
105
+ # Generates a contextualized URL of the next page for the request
88
106
  def next_link_for(records)
89
107
  uri = URI.parse(request.original_url)
90
108
  query = Rack::Utils.parse_query(uri.query)
@@ -93,7 +111,7 @@ module Apicasso
93
111
  uri.to_s
94
112
  end
95
113
 
96
- # Generates a contextualized URL of the previous page for this request
114
+ # Generates a contextualized URL of the previous page for the request
97
115
  def previous_link_for(records)
98
116
  uri = URI.parse(request.original_url)
99
117
  query = Rack::Utils.parse_query(uri.query)
@@ -103,11 +121,15 @@ module Apicasso
103
121
  end
104
122
 
105
123
  # Receives a `:action, :resource, :object` hash to validate authorization
124
+ # Example:
125
+ # > authorize_for action: :read, resource: :object_class, object: :object
106
126
  def authorize_for(opts = {})
107
127
  authorize! opts[:action], opts[:resource] if opts[:resource].present?
108
128
  authorize! opts[:action], opts[:object] if opts[:object].present?
109
129
  end
110
130
 
131
+ # @TODO
132
+ # Remove this in favor of a more controllable aproach of CORS
111
133
  def set_access_control_headers
112
134
  response.headers['Access-Control-Allow-Origin'] = request.headers["Origin"]
113
135
  response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
@@ -116,6 +138,7 @@ module Apicasso
116
138
  response.headers['Access-Control-Max-Age'] = '1728000'
117
139
  end
118
140
 
141
+ # Checks if current request is a CORS preflight check
119
142
  def preflight?
120
143
  request.request_method == 'OPTIONS' &&
121
144
  !request.headers['Authorization'].present?
@@ -4,7 +4,7 @@ module Apicasso
4
4
  # Controller to consume read-only data to be used on client's frontend
5
5
  class CrudController < Apicasso::ApplicationController
6
6
  before_action :set_root_resource
7
- before_action :set_object, except: %i[index schema create]
7
+ before_action :set_object, only: %i[show update destroy]
8
8
  before_action :set_nested_resource, only: %i[nested_index]
9
9
  before_action :set_records, only: %i[index nested_index]
10
10
 
@@ -13,11 +13,11 @@ module Apicasso
13
13
  # GET /:resource
14
14
  # Returns a paginated, ordered and filtered query based response.
15
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
16
+ # To get all `Channel` sorted by ascending `name` , filtered by
17
+ # the ones that have a `domain` that matches exactly `"domain.com"`,
18
+ # paginating records 42 per page and retrieving the page 42.
19
+ # Example:
20
+ # GET /sites?sort=+name,-updated_at&q[domain_eq]=domain.com&page=42&per_page=42
21
21
  def index
22
22
  set_access_control_headers
23
23
  render json: index_json
@@ -51,7 +51,7 @@ module Apicasso
51
51
  resource: resource.name.underscore.to_sym,
52
52
  object: @object)
53
53
  if @object.destroy
54
- head :no_content, status: :ok
54
+ head :no_content, status: :ok
55
55
  else
56
56
  render json: @object.errors, status: :unprocessable_entity
57
57
  end
@@ -62,7 +62,7 @@ module Apicasso
62
62
 
63
63
  # POST /:resource
64
64
  def create
65
- @object = resource.new(resource_params)
65
+ @object = resource.new(object_params)
66
66
  authorize_for(action: :create,
67
67
  resource: resource.name.underscore.to_sym,
68
68
  object: @object)
@@ -153,7 +153,7 @@ module Apicasso
153
153
  # or a grouped count of attributes
154
154
  def index_json
155
155
  if params[:group].present?
156
- @records.group(params[:group][:by].split(',')).send(params[:group][:calculate], params[:group][:fields])
156
+ @records.group(params[:group][:by].split(',')).send(params[:group][:calculate], params[:group][:fields]||params[:group][:by].split(','))
157
157
  else
158
158
  collection_response
159
159
  end
@@ -1,3 +1,3 @@
1
1
  module Apicasso
2
- VERSION = '0.3.2'
2
+ VERSION = '0.3.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apicasso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fernando Bellincanta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-10 00:00:00.000000000 Z
11
+ date: 2018-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cancancan