schema_based_api 2.1.13 → 2.1.14

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
  SHA256:
3
- metadata.gz: f239cdd4d05888e49c24e79d16ad1b4a4393c88b4d814ec25e4ceacf420a6345
4
- data.tar.gz: 16e577996622c20a25a3c2ba880741d7dcb0886cae85c1c20c969c598a6d8def
3
+ metadata.gz: 8471d0089a6858a8fc54493fcd2e1d10ec3cf82d021ed347645dc999c1e0ec5e
4
+ data.tar.gz: cfac408c1ffdff059610eaa98aa40e093705c74f74953f886abb792f622c2ac4
5
5
  SHA512:
6
- metadata.gz: 85373c1c2e55ef4228e7b44fc47fe4e4ecfb29eab5bda478b58c0570cbd66660a6c44bd560286121320c26ce4d5d69d4839a875c8d6c0293c276eec6601ac738
7
- data.tar.gz: 0ffc2453dc4644534e2382fc69f1a33dfad2a45527a85e2921e89bfe094c75d063d45ede997ac36196cdcc0a47c89da15227eee1a6bdeddde11257f22201722b
6
+ metadata.gz: 1695890938108ed0b87b9334c945bd6277a9784934ef6c9bab9a5def8d82aac2a49fb1f2cd649f67aeec2425b226a24767413418999ab745a3d1e822e8222646
7
+ data.tar.gz: ad5835d2ff8a1fcd14cb4b4e49400401cab586283d874c6fe83c54c1c0a40f68574aa089bef8116041a3287d1f65d9da873a08e641f4fd414bf1df66f3404065
data/README.md CHANGED
@@ -1,28 +1,25 @@
1
- # SchemaBasedApi
2
- I've always been a interested in effortless, no-fuss, conventions' based development, DRYness, and pragmatic programming, I've always thought that at this point of the technology evolution, we need not to configure too much to have our software run, and have the software adapt to data layers, from there build up automatically the APIs, visualizations etc. This is a first step to have a schema driven API, based on the data it has to serve, it also gives, thanks to meta programming, an insight on the actual schema, the translations available and the DSL which can change the way the data is presented, leading to a strong base for automatically build of UIs consuming the API (react, vue, angular based PWAs, maybe! ;-) ).
1
+ # Schema Based Api
2
+ I've always been interested in effortless, no-fuss, conventions' based development, DRYness, and pragmatic programming, I've always thought that at this point of the technology evolution, we need not to configure too much to have our software run, having the software adapt to data layers and from there building up APIs, visualizations, etc. in an automatic way. This is a first step to have a schema driven API or better model drive, based on the underlining database, the data it has to serve and some sane dafults, or conventions. This effort also gives, thanks to meta programming, an insight on the actual schema, via the info API, the translations available and the DSL which can change the way the data is presented, leading to a strong base for automatica built of UIs consuming the API (react, vue, angular based PWAs, maybe! ;-) ).
3
3
 
4
- Doing this means also narrowing a bit the scope of the tools, taking decisions, at least for the first iteratons of the project, so, this works well if the data is relational, so this is a convention taken as a prerequisite (postgres, mysql, mssql, etc.).
4
+ Doing this means also narrowing a bit the scope of the tools, taking decisions, at least for the first implementations and versions of this engine, so, this works well if the data is relational, this is a prerequisite (postgres, mysql, mssql, etc.).
5
5
 
6
6
  # Goal
7
7
 
8
- To have a comprehensive and meaningful API right out of the box by just creating migrations in your rails app.
8
+ To have a comprehensive and meaningful API right out of the box by just creating migrations in your rails app or engine.
9
9
 
10
10
  # v2?
11
11
 
12
- Yes, the [v1](https://github.com/gabrieletassoni/thecore_api) was were it all started, many ideas are ported from there, but it was too coupled with thecore's rails_admin UI, making it impossible to create an UI-less, API only application, out of the box and directly from the DB schema, with all the bells and whistles I needed (mainly self adapting, data and schema driven API functionalities).
12
+ Yes, this is the second version of such an effort and you can note it from the api calls, which are all under the ```/api/v2``` namespace the [/api/v1](https://github.com/gabrieletassoni/thecore_api) one, was were it all started, many ideas are ported from there, such as the generation of the automatic model based crud actions, as well as custom actions definitions and all the things that make also this gem useful for my daily job were already in place, but it was too coupled with [thecore](https://github.com/gabrieletassoni/thecore)'s [rails_admin](https://github.com/sferik/rails_admin) UI, making it impossible to create a complete UI-less, API only application, out of the box and directly based of the DB schema, with all the bells and whistles I needed (mainly self adapting, data and schema driven API functionalities).
13
+ So it all began again, making a better thecore_api gem into this schema_based_api gem, more polished, more functional and self contained.
13
14
 
14
15
  # Standards Used
15
16
 
16
- * [JWT](https://medium.com/@billy.sf.cheng/a-rails-6-application-part-1-api-1ee5ccf7ed01) for authentication.
17
+ * [JWT](https://github.com/jwt/ruby-jwt) for authentication.
17
18
  * [CanCanCan](https://github.com/CanCanCommunity/cancancan) for authorization.
18
19
  * [Active Hash Relation](https://github.com/kollegorna/active_hash_relation) for DSL.
19
20
  * [Ransack](https://github.com/activerecord-hackery/ransack) query engine for complex searches going beyond CRUD's listing scope.
20
21
  * Catch all routing rule to add basic crud operations to any AR model in the app.
21
22
 
22
- # TODO
23
-
24
- * Integrate Authorization within ```GET info/schema``` requests in order to send to the client just the models for which a user has authorization.
25
-
26
23
  ## Usage
27
24
  How to use my plugin.
28
25
 
@@ -53,11 +50,15 @@ This will setup a User model, Role model and the HABTM table between the two.
53
50
  Then, if you fire up your ```rails server``` you can already get a jwt and perform different operations.
54
51
  The default admin user created during the migration step has a randomly generated password you can find in a .passwords file in the root of your project, that's the initial password, in production you can replace that one, but for testing it proved handy to have it promptly available.
55
52
 
56
- If you want to manually test the API using [Insomnia](https://insomnia.rest/)
53
+ ## Testing
54
+
55
+ If you want to manually test the API using [Insomnia](https://insomnia.rest/) I will publish in the repository the export for the chained requests I'm using.
56
+ In the next few days, I'll publish also the rspec tests.
57
57
 
58
58
  ## References
59
59
  THanks to all these people for ideas:
60
60
 
61
+ * [Billy Cheng](https://medium.com/@billy.sf.cheng/a-rails-6-application-part-1-api-1ee5ccf7ed01) For a way to have a nice and clean implementation of the JWT on top of Devise.
61
62
  * [Daniel](https://medium.com/@tdaniel/passing-refreshed-jwts-from-rails-api-using-headers-859f1cfe88e9) For a smart way to manage token expiration.
62
63
 
63
64
  ## License
@@ -1,7 +1,7 @@
1
1
  class AuthenticateUser
2
2
  class AccessDenied < StandardError
3
3
  def message
4
- "Cannot authenticate user."
4
+ "AuthenticationError"
5
5
  end
6
6
  end
7
7
  prepend SimpleCommand
@@ -17,82 +17,15 @@ class Api::V2::ApplicationController < ActionController::API
17
17
  request.parameters
18
18
  end
19
19
 
20
- # TODO: Remove when not needed
21
- # def dispatcher
22
- # # This method is only valid for ActiveRecords
23
- # # For any other model-less controller, the actions must be
24
- # # defined in the route, and must exist in the controller definition.
25
- # # So, if it's not an activerecord, the find model makes no sense at all.
26
- # path = params[:path].split("/")
27
- # # Default convention for the requests: :controller/:id/:custom_action
28
- # # or :controller/:custom_action.
29
- # # With the ID as an Integer
30
- # # TODO: Extend to understand nested resources maybe testing if the
31
- # # third param is a AR model, that can have an ID, etc..
32
- # controller = path.first
33
- # id = path.second
34
- # custom_action = path.third
35
- # # managing
36
- # if request.get?
37
- # if id.blank?
38
- # # @page = params[:page]
39
- # # @per = params[:per]
40
- # # @pages_info = params[:pages_info]
41
- # # @count = params[:count]
42
- # # @query = params[:q]
43
- # index
44
- # elsif id.to_i.zero?
45
- # # String, so it's a custom action I must find in the @model (as a singleton method)
46
- # # GET :controller/:custom_action
47
- # return not_found! unless @model.respond_to?(id)
48
- # return render json: MultiJson.dump(@model.send(id, params)), status: 200
49
- # elsif !id.to_i.zero? && custom_action.blank?
50
- # # Integer, so it's an ID, I must show it
51
- # @record_id = id.to_i
52
- # find_record
53
- # show
54
- # elsif !id.to_i.zero? && !custom_action.blank?
55
- # # GET :controller/:id/:custom_action
56
- # return not_found! unless @model.respond_to?(custom_action)
57
- # return render json: MultiJson.dump(@model.send(custom_action, id.to_i, params)), status: 200
58
- # end
59
- # elsif request.post?
60
- # if id.blank?
61
- # # @params = params
62
- # create
63
- # elsif id.to_i.zero?
64
- # # POST :controller/:custom_action
65
- # return not_found! unless @model.respond_to?(id)
66
- # return render json: MultiJson.dump(@model.send(id, params)), status: 200
67
- # end
68
- # elsif request.put?
69
- # if !id.to_i.zero? && custom_action.blank?
70
- # # @params = params
71
- # # Rails.logger.debug "IL SECONDO è ID in PUT? #{path.second.inspect}"
72
- # # find_record path.second.to_i
73
- # @record_id = id.to_i
74
- # find_record
75
- # update
76
- # elsif !id.to_i.zero? && !custom_action.blank?
77
- # # PUT :controller/:id/:custom_action
78
- # # puts "ANOTHER SECOND AND THIRD"
79
- # return not_found! unless @model.respond_to?(custom_action)
80
- # return render json: MultiJson.dump(@model.send(custom_action, id.to_i, params)), status: 200
81
- # end
82
- # elsif request.delete?
83
- # # Rails.logger.debug "IL SECONDO è ID in delete? #{path.second.inspect}"
84
- # # find_record path.second.to_i
85
- # @record_id = id.to_i
86
- # find_record
87
- # destroy
88
- # end
89
- # end
90
-
91
20
  # GET :controller/
92
21
  def index
93
22
  authorize! :index, @model
94
- # Rails.logger.debug params.inspect
95
- # find the records
23
+
24
+ # Custom Action
25
+ status, result = check_for_custom_action
26
+ return render json: result, status: 200 if status == true
27
+
28
+ # Normal Index Action with Ransack querying
96
29
  @q = (@model.column_names.include?("user_id") ? @model.where(user_id: current_user.id) : @model).ransack(@query.presence|| params[:q])
97
30
  @records_all = @q.result(distinct: true)
98
31
  page = (@page.presence || params[:page])
@@ -119,7 +52,13 @@ class Api::V2::ApplicationController < ActionController::API
119
52
  end
120
53
 
121
54
  def show
122
- authorize! :show, @record
55
+ authorize! :show, @record_id
56
+
57
+ # Custom Show Action
58
+ status, result = check_for_custom_action
59
+ return render json: result, status: 200 if status == true
60
+
61
+ # Normal Show
123
62
  result = @record.to_json(json_attrs)
124
63
  render json: result, status: 200
125
64
  end
@@ -127,32 +66,60 @@ class Api::V2::ApplicationController < ActionController::API
127
66
  def create
128
67
  @record = @model.new(@body)
129
68
  authorize! :create, @record
69
+
70
+ # Custom Action
71
+ status, result = check_for_custom_action
72
+ return render json: result, status: 200 if status == true
73
+
74
+ # Normal Create Action
130
75
  @record.user_id = current_user.id if @model.column_names.include? "user_id"
131
-
132
76
  @record.save!
133
-
134
77
  render json: @record.to_json(json_attrs), status: 201
135
78
  end
136
79
 
137
80
  def update
138
81
  authorize! :update, @record
82
+
83
+ # Custom Action
84
+ status, result = check_for_custom_action
85
+ return render json: result, status: 200 if status == true
86
+
87
+ # Normal Update Action
139
88
  @record.update_attributes!(@body)
140
-
141
89
  render json: @record.to_json(json_attrs), status: 200
142
90
  end
143
91
 
144
92
  def destroy
145
93
  authorize! :destroy, @record
94
+
95
+ # Custom Action
96
+ status, result = check_for_custom_action
97
+ return render json: result, status: 200 if status == true
98
+
99
+ # Normal Destroy Action
146
100
  return api_error(status: 500) unless @record.destroy
147
101
  head :ok
148
102
  end
149
103
 
150
104
  private
105
+
106
+ def check_for_custom_action
107
+ ## CUSTOM ACTION
108
+ # [GET|PUT|POST|DELETE] :controller?do=:custom_action
109
+ # or
110
+ # [GET|PUT|POST|DELETE] :controller/:id?do=:custom_action
111
+ unless params[:do].blank?
112
+ raise NoMethodError unless @model.respond_to?(params[:do])
113
+ return true, MultiJson.dump(params[:id].blank? ? @model.send(params[:do], params) : @model.send(params[:do], params[:id].to_i, params))
114
+ end
115
+ # if it's here there is no custom action in the request querystring
116
+ return false
117
+ end
151
118
 
152
119
  def authenticate_request
153
120
  @current_user = AuthorizeApiRequest.call(request.headers).result
154
121
  return unauthenticated! unless @current_user
155
- current_user = @current_user
122
+ params[:current_user] = current_user = @current_user
156
123
  # Now every time the user fires off a successful GET request,
157
124
  # a new token is generated and passed to them, and the clock resets.
158
125
  response.headers['Token'] = JsonWebToken.encode(user_id: current_user.id)
@@ -6,9 +6,7 @@ class Api::V2::AuthenticationController < ActionController::API
6
6
 
7
7
  if command.success?
8
8
  response.headers['Token'] = command.result
9
- render json: { message: ["Login successful!"] }
10
- else
11
- render json: { error: command.errors }, status: :unauthorized
9
+ head :ok
12
10
  end
13
11
  end
14
12
 
@@ -10,34 +10,25 @@ module ApiExceptionManagement
10
10
  rescue_from ActiveRecord::RecordInvalid, with: :invalid!
11
11
  rescue_from ActiveRecord::RecordNotFound, with: :not_found!
12
12
 
13
- def unauthenticated! exception = StandardError.new
13
+ def unauthenticated! exception = AuthenticateUser::AccessDenied.new
14
14
  response.headers['WWW-Authenticate'] = "Token realm=Application"
15
- api_error status: 401, errors: [I18n.t("api.errors.bad_credentials", default: "Bad Credentials"), exception.message]
15
+ return api_error status: 401, errors: exception.message
16
16
  end
17
17
 
18
- def unauthorized! exception = StandardError.new
19
- api_error status: 403, errors: [I18n.t("api.errors.unauthorized", default: "Unauthorized"), exception.message]
20
- return
18
+ def unauthorized! exception = CanCan::AccessDenied.new
19
+ return api_error status: 403, errors: exception.message
21
20
  end
22
21
 
23
22
  def not_found! exception = StandardError.new
24
- return api_error(status: 404, errors: [I18n.t("api.errors.not_found", default: "Not Found"), exception.message])
25
- end
26
-
27
- def name_error!
28
- api_error(status: 501, errors: [I18n.t("api.errors.name_error", default: "Name Error")])
29
- end
30
-
31
- def no_method_error!
32
- api_error(status: 501, errors: [I18n.t("api.errors.no_method_error", default: "No Method Error")])
23
+ return api_error status: 404, errors: exception.message
33
24
  end
34
25
 
35
26
  def invalid! exception = StandardError.new
36
- api_error status: 422, errors: exception.record.errors
27
+ return api_error status: 422, errors: exception.record.errors
37
28
  end
38
29
 
39
30
  def fivehundred! exception = StandardError.new
40
- api_error status: 500, errors: [I18n.t("api.errors.fivehundred", default: "Internal Server Error"), exception.message]
31
+ return api_error status: 500, errors: exception.message
41
32
  end
42
33
 
43
34
  def api_error(status: 500, errors: [])
@@ -1,3 +1,3 @@
1
1
  module SchemaBasedApi
2
- VERSION = '2.1.13'
2
+ VERSION = '2.1.14'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema_based_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.13
4
+ version: 2.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriele Tassoni
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-17 00:00:00.000000000 Z
11
+ date: 2020-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -205,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
205
  - !ruby/object:Gem::Version
206
206
  version: '0'
207
207
  requirements: []
208
- rubygems_version: 3.1.2
208
+ rubygems_version: 3.0.6
209
209
  signing_key:
210
210
  specification_version: 4
211
211
  summary: Convention based RoR engine which uses DB schema introspection to create