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 +4 -4
- data/README.md +12 -11
- data/app/commands/authenticate_user.rb +1 -1
- data/app/controllers/api/v2/application_controller.rb +45 -78
- data/app/controllers/api/v2/authentication_controller.rb +1 -3
- data/lib/concerns/api_exception_management.rb +7 -16
- data/lib/schema_based_api/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8471d0089a6858a8fc54493fcd2e1d10ec3cf82d021ed347645dc999c1e0ec5e
|
4
|
+
data.tar.gz: cfac408c1ffdff059610eaa98aa40e093705c74f74953f886abb792f622c2ac4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1695890938108ed0b87b9334c945bd6277a9784934ef6c9bab9a5def8d82aac2a49fb1f2cd649f67aeec2425b226a24767413418999ab745a3d1e822e8222646
|
7
|
+
data.tar.gz: ad5835d2ff8a1fcd14cb4b4e49400401cab586283d874c6fe83c54c1c0a40f68574aa089bef8116041a3287d1f65d9da873a08e641f4fd414bf1df66f3404065
|
data/README.md
CHANGED
@@ -1,28 +1,25 @@
|
|
1
|
-
#
|
2
|
-
I've always been
|
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
|
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
|
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://
|
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
|
-
|
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
|
@@ -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
|
-
|
95
|
-
#
|
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, @
|
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
|
-
|
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 =
|
13
|
+
def unauthenticated! exception = AuthenticateUser::AccessDenied.new
|
14
14
|
response.headers['WWW-Authenticate'] = "Token realm=Application"
|
15
|
-
api_error status: 401, errors:
|
15
|
+
return api_error status: 401, errors: exception.message
|
16
16
|
end
|
17
17
|
|
18
|
-
def unauthorized! exception =
|
19
|
-
api_error status: 403, errors:
|
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
|
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:
|
31
|
+
return api_error status: 500, errors: exception.message
|
41
32
|
end
|
42
33
|
|
43
34
|
def api_error(status: 500, errors: [])
|
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.
|
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-
|
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.
|
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
|