dash_api 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 945983e9aaba31d04f8fde011da5a657e2d0d6159e6586e83ba8f6c03fa6653f
4
+ data.tar.gz: 24bc114dc3585acf020fcca25d0795a7da97edab9b433d37913fb9cbd99b7afb
5
+ SHA512:
6
+ metadata.gz: 68eba8c684f9193eeb6495852659941655d66ef24a9ac7ae33201fa9e8e93ad384476eef60e9060ca8771b846a634f4b24b40bc36cd2b5bffcf8dcda0eae467e
7
+ data.tar.gz: d29484f82a3715c8af5fe7703e0115818c04ecd2a9be2ac5dd4eeaab672c0d7a4879990560cfb280e9c674de8925cff31fc125e8e9f94a965f109107be6474e4
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Rami Bitar
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,406 @@
1
+ # Dash API
2
+ Dash API is a Rails engine that mounts an instant REST API for your Ruby on Rails applications
3
+ using your Postgres database. DashAPI can be queried using a flexible and expressive syntax from URL parameters.
4
+
5
+ DashAPI is also designed to be performant, scalable and secure.
6
+
7
+ Note: DashAPI is a pre-release product and not yet recommended for any production applications.
8
+
9
+ ## Features
10
+ DashAPI is an instant REST API for your Postgres database built using Ruby on Rails.
11
+ DashAPI supports several features out of the box with little or no configuration required:
12
+ - Full-text search
13
+ - Filtering
14
+ - Sorting
15
+ - Selects
16
+ - Associations
17
+ - Pagination
18
+ - JWT token authorization
19
+
20
+ DashAPI is designed to help rapidly build fully functional, scalable and secure applications
21
+ by automatically generating REST APIs using any Postgres database. DashAPI also supports
22
+ advanced features including join associations between tables, full-text keyword search using the
23
+ native search capabilities of postgres, and
24
+
25
+ ## Installation
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'dash_api'
30
+ ```
31
+
32
+ And then execute:
33
+ ```bash
34
+ $ bundle
35
+ ```
36
+
37
+ Mount the Dash API by updating your `routes.rb` file:
38
+ ```
39
+ mount DashApi::Engine, at: "/dash/api"
40
+ ```
41
+
42
+ Or install it yourself as:
43
+ ```bash
44
+ $ gem install dash_api
45
+ ```
46
+
47
+ You can also configure DashApi with an initializer by creating a file at `config/initializers/dash_api.rb`.
48
+
49
+ ```
50
+ # Place file at config/initializers/dash_api.rb
51
+
52
+ DashApi.tap |config|
53
+ config.enable_auth = ENV['DASH_ENABLE_AUTH'] === true
54
+ config.api_token = ENV['DASH_API_TOKEN']
55
+ config.jwt_secret = ENV['DASH_JWT_SECRET']
56
+ config.exclude_fields = ENV['DASH_API_TOKEN'].split(" ") || []
57
+ config.exclude_tables = ENV['DASH_API_TOKEN'].split(" ") || []
58
+ end
59
+ ```
60
+ The DashAPI is now ready. You can view all tables at:
61
+ `/dash/api`
62
+
63
+ You can query a table at:
64
+ `/dash/api/<table_name>`
65
+
66
+ ## Requirements
67
+
68
+ Dash API requires Ruby on Rails and Postgres, and below are recommended versions:
69
+ - Rails 6+
70
+ - Ruby 2.7.4+
71
+ - Postgres 9+ database
72
+
73
+ ## Documentation
74
+
75
+ Dash supports a flexible, expressive query syntax using URL parameters to query a Postgres database.
76
+
77
+ All tables can be queried by passing in the table name to the dash API endpoint where you mounted
78
+ the Dash rails engine:
79
+
80
+ ```
81
+ GET /dash/api/<table>
82
+ ```
83
+
84
+ Example:
85
+
86
+ ```
87
+ GET /dash/api/books
88
+ ```
89
+
90
+ ### Schema
91
+
92
+ Your database schema is available in order to inspect available tables and column data.
93
+
94
+ ```
95
+ GET /dash/api/schema
96
+ ```
97
+
98
+ You can inspect any specific table using the schema endpoint:
99
+
100
+ ```
101
+ GET /dash/api/schema/<table_name>
102
+ ```
103
+
104
+ Example:
105
+ ```
106
+ GET /dash/api/schema/books
107
+ ```
108
+
109
+ ### Filtering
110
+
111
+ You can filter queries using the pattern
112
+
113
+ ```
114
+ GET /dash/api/table?filters=<resource_field>:<operator>:<value>
115
+ ```
116
+
117
+ Example:
118
+ Below is an example to find all users with ID less than 10:
119
+ ```
120
+ GET /dash/api/books?filters=id:lt:10
121
+ ```
122
+ You can also chain filters to support multiple filters that are ANDed together:
123
+
124
+ ```
125
+ GET /dash/api/books?filters=id:lt:10,published:eq:true
126
+ ```
127
+
128
+ The currently supported operators are:
129
+ ```
130
+ eq - Equals
131
+ neq - Not equals
132
+ gt - Greater than
133
+ gte - Greater than or equal too
134
+ lt - Less than
135
+ lte - Less than or equal too
136
+ ```
137
+
138
+ ### Sorting
139
+
140
+ You can sort queries using the pattern:
141
+
142
+ ```
143
+ GET /dash/api/table?order=<field>:<asc|desc>
144
+ ```
145
+
146
+ Example:
147
+
148
+ ```
149
+ GET /dash/api/books?order=title:desc
150
+ ```
151
+
152
+ ### Pagination
153
+
154
+ Dash API uses page based pagination. By default results are paginated with 20 results per page.
155
+ You can paginate results with the following query pattern:
156
+
157
+ ```
158
+ GET /dash/api/table?page=<page>&per_page=<per_page>
159
+ ```
160
+
161
+ Example:
162
+ ```
163
+ GET /dash/api/books?page=2&per_page=10
164
+ ```
165
+
166
+ ### Select
167
+
168
+ Select fields allow you to return only specific fields from a table, similar to the SQL select statement.
169
+ Select fields follows the query pattern:
170
+
171
+ ```
172
+ GET /dash/api/table?select=<field>,<field>
173
+ ```
174
+
175
+ Example:
176
+ You can comma separate the fields to include multiple fields in the response
177
+
178
+ ```
179
+ GET /dash/api/books?select=id,title,summary
180
+ ```
181
+
182
+ ### Full-text search
183
+
184
+ DashAPI supports native full-text search capabilities. You can search against all fields of a tables with the
185
+ query syntax:
186
+
187
+ ```
188
+ GET /dash/api/table?keywords=<search_terms>
189
+ ```
190
+
191
+ Example:
192
+ You can find all users that match the search term below. All keywords are URI decoded prior to issuing a search.
193
+ ```
194
+ GET /dash/api/books?keywords=ruby+on+rails
195
+ ```
196
+
197
+ Warning: At this time all fields are searchable and using this API which may include any sensitive data in your database.
198
+
199
+
200
+ ### Associations
201
+
202
+ Dash takes advantage of Ruby on Rails expressive and powerful ORM, ActiveRecord, to
203
+ dymacally infer associations between tables and serialize them. Associations currently
204
+ supported are belongs_to and has_many, and this feature is only available
205
+ for tables that follow strict Rails naming conventions.
206
+
207
+ To include a belongs_to table association, use the singular form of the table name:
208
+ ```
209
+ GET /dash/api/books?includes=author
210
+ ```
211
+
212
+ To include a has_manay table association, use the plural form of the table name:
213
+ ```
214
+ GET /dash/api/books?includes=reviews
215
+ ```
216
+
217
+ To combine associations together comma seperate the included tables:
218
+
219
+ ```
220
+ GET /dash/api/books?includes=author,reviews
221
+ ```
222
+
223
+
224
+ ### Create
225
+
226
+ Create table rows:
227
+
228
+ ```
229
+ POST /dash/api/<table_name>
230
+
231
+ Body
232
+
233
+ {
234
+ <table_name>: {
235
+ field: value,
236
+ ...
237
+ }
238
+ }
239
+ ```
240
+
241
+ ### Update
242
+
243
+ Update table rows by ID:
244
+
245
+ ```
246
+ PUT /dash/api/<table_name>/<id>
247
+
248
+ Body
249
+
250
+ {
251
+ <table_name>: {
252
+ field: value,
253
+ ...
254
+ }
255
+ }
256
+
257
+ ```
258
+
259
+ ### Delete
260
+
261
+ Delete table rows by id:
262
+
263
+ ```
264
+ DELETE /dash/api/<table_name>/<id>
265
+ ```
266
+
267
+ ### Update many
268
+
269
+ Bulk update multiple rows by passing in an array of integers the the JSON attributes to update:
270
+
271
+ ```
272
+ POST /dash/api/<table_name>/update_many
273
+
274
+ Body
275
+
276
+ {
277
+ ids: [Integer],
278
+ <table_name>: {
279
+ field: value,
280
+ ...
281
+ }
282
+ }
283
+ ```
284
+
285
+ ### Delete many
286
+
287
+ Bulk delete rows by passing in an array of IDs to delete:
288
+
289
+ ```
290
+ POST /dash/api/<table_name>/delete_many
291
+
292
+ Body
293
+
294
+ {
295
+ ids: [Integer]
296
+ }
297
+ ```
298
+
299
+ ### API Token Authentication
300
+
301
+ You may secure the Dash API using a dedicated API token or using a JWT token.
302
+
303
+ For a fast and simple way to secure your API, you can specify an api_token in `config/initializers/dash_api.rb` which will allow all API requests.
304
+
305
+
306
+ ```
307
+ # /config/initializers/dash_api.rb
308
+
309
+ DashApi.tap do |config|
310
+ config.enable_auth = true
311
+ config.api_token = ENV['DASH_API_TOKEN']
312
+ ...
313
+ end
314
+ ```
315
+
316
+ Ensure that the enable authentication flag `enable_auth` is set to `true`.
317
+
318
+
319
+ ### JWT Token Authentication
320
+
321
+ The recommended and preferred way to secure your API is to use a JWT token. To enable a JWT token, you must first
322
+ specify the JWT secret key in your `config/initializers/dash_api.rb`
323
+
324
+ Note that if your Dash API works alongside an existing API or an additional server which handles authentication, you can use a shared JWT secret that is used by both services to decode the JWT token. The JWT tokenization
325
+ uses the RS
326
+
327
+
328
+ ```
329
+ # /config/initializers/dash_api.rb
330
+
331
+ DashApi.tap do |config|
332
+ config.enable_auth = true
333
+ config.jwt_secret = ENV['DASH_JWT_SECRET']
334
+ ...
335
+ end
336
+ ```
337
+
338
+ Ensure that the enable authentication flag `enable_auth` is set to `true`.
339
+
340
+ The JWT token will also inspect for the `exp` key and if present will only allow requests with valid
341
+ expiration timestamps. For security purposes it's recommended that you encode your JWT tokens with an exp
342
+ timestamp.
343
+
344
+ To setup and test JWT tokens, we recommend you explore [jwt.io](https://jwt.io).
345
+
346
+ ### API Authorization
347
+
348
+ Once you've setup your authentication strategy above, either using the `api_token` or the `jwt_secret`,
349
+ you should pass your token to the DashAPI either as a URL parameter or using Bearer authentication
350
+
351
+ You can pass the token as a url paramter:
352
+ ```
353
+ ?token=<JWT_TOKEN>
354
+ ```
355
+
356
+ The preferred strategy is to pass the token using Bearer authentication in your headers:
357
+ ```
358
+ Authorization: 'Bearer <JWT_TOKEN>'
359
+ ```
360
+
361
+
362
+ ### Exclude fields and tables
363
+
364
+ The Dash API is not yet suitable for production scale applications. Please use with caution.
365
+
366
+ You can exclude fields from being serialized by specifying DASH_EXCLUDE_FIELDS
367
+
368
+ ```
369
+ # /config/initializers/dash_api.rb
370
+ DashApi.tap do |config|
371
+ ...
372
+ config.exclude_fields = ENV['DASH_EXCLUDE_FIELDS'].split(' ') || []
373
+ ...
374
+ end
375
+ ```
376
+
377
+ Example:
378
+ ```
379
+ config.exclude_fields = "encrypted_password hashed_password secret_token"
380
+ ```
381
+
382
+ You can also exclude tables from the API using the exclude_tables configuration:
383
+
384
+ ```
385
+ # /config/initializers/dash_api.rb
386
+ DashApi.tap do |config|
387
+ ...
388
+ config.exclude_tables = ENV['DASH_EXCLUDE_TABLES'].split(' ') || []
389
+ ...
390
+ end
391
+ ```
392
+
393
+ Example:
394
+ ```
395
+ config.exclude_tables = "api_tokens users private_notes"
396
+ ```
397
+
398
+
399
+ ## Contributing
400
+ Contributions are welcome by issuing a pull request at our github repository:
401
+ https:/github.com/skillhire/dash_api
402
+
403
+
404
+ ## License
405
+
406
+ The gem is available as open source under the terms of the [MIT License](https:/opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/dash_api.css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,89 @@
1
+ module DashApi
2
+ class ApiController < ApplicationController
3
+
4
+ skip_before_action :verify_authenticity_token
5
+
6
+ before_action :authenticate_request!
7
+ before_action :load_table
8
+ before_action :parse_query_params
9
+
10
+ def index
11
+ @dash_table.query(
12
+ keywords: @query[:keywords],
13
+ select_fields: @query[:select_fields],
14
+ sort_by: @query[:sort_by],
15
+ sort_direction: @query[:sort_direction],
16
+ filters: @query[:filters],
17
+ associations: @query[:associations],
18
+ page: @query[:page],
19
+ per_page: @query[:per_page]
20
+ )
21
+
22
+ render json: {
23
+ data: @dash_table.serialize,
24
+ meta: @dash_table.page_info
25
+ }
26
+ end
27
+
28
+ def show
29
+ resource = @dash_table.query(
30
+ associations: @query[:associations],
31
+ filters: [{ field: :id, operator: "=", value: params[:id] }],
32
+ page: 1,
33
+ per_page: 1
34
+ )
35
+ render json: { data: @dash_table.serialize[0] }
36
+ end
37
+
38
+ def create
39
+ resource = @dash_table.create!(dash_params)
40
+ render json: { data: resource }
41
+ end
42
+
43
+ def update
44
+ resource = @dash_table.find(params[:id])
45
+ if resource.update(dash_params)
46
+ render json: { data: resource }
47
+ else
48
+ render json: { error: resource.errors.full_messages }, status: 422
49
+ end
50
+ end
51
+
52
+ def destroy
53
+ resource = @dash_table.find(params[:id])
54
+ resource.destroy
55
+ render json: { data: resource }
56
+ end
57
+
58
+ def update_many
59
+ resources = @dash_table.where(id: params[:ids])
60
+ resources.update(dash_params)
61
+ render json: { data: resources }
62
+ end
63
+
64
+ def delete_many
65
+ resources = @dash_table.where(id: params[:ids])
66
+ resources.destroy_all
67
+ render json: { data: resources }
68
+ end
69
+
70
+ private
71
+
72
+ def parse_query_params
73
+ @query = DashApi::Query.parse(params)
74
+ end
75
+
76
+ def load_table
77
+ raise "This resource is excluded." if DashApi.exclude_tables.include?(params[:table_name])
78
+ @dash_table = DashTable.modelize(params[:table_name])
79
+ @dash_table.table_name = params[:table_name]
80
+ end
81
+
82
+ def dash_params
83
+ params
84
+ .require(params[:table_name])
85
+ .permit!
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,42 @@
1
+ module DashApi
2
+ class ApplicationController < ActionController::Base
3
+
4
+ rescue_from Exception, with: :unprocessable_entity
5
+ rescue_from StandardError, with: :unprocessable_entity
6
+ rescue_from ActiveRecord::RecordNotFound, with: :unprocessable_entity
7
+ rescue_from ActiveRecord::ActiveRecordError, with: :unprocessable_entity
8
+
9
+
10
+ def authenticate_request!
11
+ if DashApi.enable_auth === true
12
+ return true if auth_token === DashApi.api_token && !DashApi.api_token.blank?
13
+ jwt_token
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def jwt_token
20
+ DashApi::JsonWebToken.decode(auth_token)
21
+ rescue JWT::ExpiredSignature
22
+ raise "JWT token has expired"
23
+ rescue JWT::VerificationError, JWT::DecodeError
24
+ raise "Invalid JWT token"
25
+ end
26
+
27
+ def auth_token
28
+ http_token || params['token']
29
+ end
30
+
31
+ def http_token
32
+ if request.headers['Authorization'].present?
33
+ request.headers['Authorization'].split(' ').last
34
+ end
35
+ end
36
+
37
+ def unprocessable_entity(e)
38
+ render json: { error: e }, status: :unprocessable_entity
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ module DashApi
2
+ class SchemaController < ApplicationController
3
+
4
+ before_action :authenticate_request!
5
+
6
+ def index
7
+ tables = DashApi::Schema.table_names
8
+ render json: { data: tables }
9
+ end
10
+
11
+ def schema
12
+ schema = DashApi::Schema.db_schema
13
+ render json: { data: schema }
14
+ end
15
+
16
+ def show
17
+ table_schema = DashApi::Schema.table_schema(params[:table_name])
18
+ render json: { data: table_schema }
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module DashApi
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module DashApi
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module DashApi
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,203 @@
1
+ module DashApi
2
+ module DashModel
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+
7
+ attr_accessor :scope, :includes
8
+
9
+ def query(
10
+ keywords: nil,
11
+ select_fields: nil,
12
+ associations: nil,
13
+ sort_by: 'id',
14
+ sort_direction: 'asc',
15
+ filters: nil,
16
+ page: 1,
17
+ per_page: 20
18
+ )
19
+
20
+ @current_scope = self.all
21
+
22
+ filter(filters: filters)
23
+
24
+ sort(sort_by: sort_by, sort_direction: sort_direction)
25
+
26
+ full_text_search(keywords: keywords)
27
+
28
+ join_associations(associations: associations)
29
+
30
+ select_fields(fields: select_fields)
31
+
32
+ paginate(page: page, per_page: per_page)
33
+
34
+ @current_scope
35
+ end
36
+
37
+ # Filtering
38
+ # Filtering follows the URL pattern <field>.<attribute>=<operator>.<value>
39
+ # Filters can also be chained to AND filter queries together
40
+ #
41
+ # Example: /books?books.id=lte.10 returns all books with id <= 10
42
+
43
+ def filter(filters: [])
44
+ filters.each do |filter|
45
+ @current_scope = @current_scope.where(["#{filter[:field]} #{filter[:operator]} ?", filter[:value]])
46
+ end
47
+ end
48
+
49
+ # Sorting
50
+ # Sort any field in ascending or descending direction
51
+ # Example: /books?order=title.asc
52
+
53
+ def sort(sort_by: nil, sort_direction: nil)
54
+ return self unless sort_by && sort_direction
55
+ @current_scope = @current_scope.order("#{sort_by}": sort_direction)
56
+ end
57
+
58
+ # Select
59
+ # Only return select fields from the table, equivalent to SQL select()
60
+ # Example: /books?select=id,title,summary
61
+
62
+ def select_fields(fields: nil)
63
+ return unless fields
64
+ @current_scope = @current_scope.select(fields)
65
+ end
66
+
67
+ # Search
68
+ # Full-text search using the pg_search gem
69
+ # pg_search_scope is assigned dynamically against all table columns
70
+ # Example: books?keywords=Ruby+on+Rails
71
+
72
+ def full_text_search(keywords: nil)
73
+ return if keywords.blank?
74
+ self.pg_search_scope(
75
+ :pg_search,
76
+ against: self.searchable_fields,
77
+ using: {
78
+ tsearch: {
79
+ any_word: false
80
+ }
81
+ }
82
+ )
83
+ results = pg_search(keywords)
84
+ @current_scope = results
85
+ end
86
+
87
+ # Paginate
88
+ # Paginate the results with an offset and limit
89
+ # Example: books?page=1&per_page=20
90
+
91
+ def paginate(per_page: 20, page: 1)
92
+ @page = page
93
+ @per_page = per_page
94
+ offset = (page-1)*per_page
95
+ @current_scope = @current_scope.limit(per_page).offset(offset)
96
+ end
97
+
98
+ # Join associations
99
+ # We infer the association as either belongs_to or has_many
100
+ # based on whether the associated table name is singular or plural.
101
+ # We dynamically create the ActiveRecord class and set the appropriate
102
+ # ActiveRecord:Relation relationships
103
+ #
104
+ # Example: books?includes=author,reviews
105
+
106
+ def join_associations(associations: nil)
107
+ return nil unless associations
108
+ @associations = associations
109
+ associations.each do |table_name|
110
+ klass = DashTable.modelize(table_name)
111
+ if is_singular?(table_name)
112
+ self.belongs_to table_name.singularize.to_sym
113
+ klass.has_many self.table_name.pluralize.to_sym
114
+ @current_scope = @current_scope.includes(table_name.singularize.to_sym)
115
+ else
116
+ self.has_many table_name.pluralize.to_sym
117
+ klass.belongs_to self.table_name.singularize.to_sym
118
+ @current_scope = @current_scope.includes(table_name.pluralize.to_sym)
119
+ end
120
+ end
121
+ end
122
+
123
+ # Serialize
124
+ # We serialize with the appropriate associations
125
+ # based on whether the assocation is singular or plural.
126
+ def serialize
127
+ if @associations
128
+ associations_sym = @associations.map{|table_name|
129
+ is_singular?(table_name) ?
130
+ table_name.singularize.to_sym :
131
+ table_name.pluralize.to_sym
132
+ }
133
+
134
+ include_hash = {}
135
+ associations_sym.each do |association|
136
+ include_hash.merge!({
137
+ "#{association}": {
138
+ except: DashApi.exclude_fields
139
+ }
140
+ })
141
+ end
142
+
143
+ @current_scope.as_json(
144
+ include: include_hash,
145
+ except: DashApi.exclude_fields
146
+ )
147
+ else
148
+ @current_scope.as_json(except: DashApi.exclude_fields)
149
+ end
150
+ end
151
+
152
+ def is_singular?(name)
153
+ name && name.singularize == name
154
+ end
155
+
156
+ def page_info
157
+ {
158
+ page: @page,
159
+ per_page: @per_page
160
+ }
161
+ end
162
+
163
+ def table_columns
164
+ return [] if self.table_name.nil?
165
+ self.columns
166
+ end
167
+
168
+ def searchable_fields
169
+ return [] if self.table_name.nil?
170
+ self.columns.map(&:name).filter{|column|
171
+ column unless DashApi.exclude_fields.include?(column.to_sym)
172
+ }
173
+ end
174
+
175
+ def modelize(table_name)
176
+ class_name = table_name.singularize.capitalize
177
+ if Object.const_defined? class_name
178
+ class_name.constantize
179
+ else
180
+ Object.const_set class_name, Class.new(DashApi::DashTable)
181
+ end
182
+ end
183
+
184
+ def foreign_keys
185
+ self.columns.map{|col|
186
+ if col.name.downcase.split("_")[-1] == 'id' && col.name.downcase != 'id'
187
+ col.name
188
+ end
189
+ }.compact
190
+ end
191
+
192
+ def foreign_table(foreign_key)
193
+ foreign_key.downcase.split('_').first.pluralize
194
+ end
195
+
196
+ def foreign_tables
197
+ foreign_keys.map{ |foreign_key|
198
+ foreign_table(foreign_key)
199
+ }
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,5 @@
1
+ module DashApi
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module DashApi
2
+ class DashTable < ApplicationRecord
3
+ include PgSearch::Model
4
+ include DashApi::DashModel
5
+
6
+ self.abstract_class = true
7
+
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module DashApi
2
+ module JsonWebToken
3
+ require 'jwt'
4
+
5
+ JWT_HASH_ALGORITHM = 'HS256'
6
+
7
+ def self.encode(payload:, expiration:)
8
+ payload[:exp] = expiration || Time.now.advance(minutes: 15)
9
+ JWT.encode(payload, DashApi.jwt_secret, DashApi.jwt_hash_algorithm ||JWT_HASH_ALGORITHM)
10
+ end
11
+
12
+ def self.decode(jwt_token)
13
+ HashWithIndifferentAccess.new(JWT.decode(jwt_token, DashApi.jwt_secret, true, {
14
+ algorithm: DashApi.jwt_algorithm || JWT_HASH_ALGORITHM
15
+ })[0])
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ module DashApi
2
+ module Query
3
+
4
+ PER_PAGE = 20
5
+
6
+ SORT_DIRECTIONS = ['asc', 'desc']
7
+
8
+ DELIMITER = ":"
9
+
10
+ OPERATORS = {
11
+ "gt": ">",
12
+ "gte": ">=",
13
+ "lt": "<",
14
+ "lte": "<=",
15
+ "eq": "=",
16
+ "neq": "!="
17
+ }
18
+
19
+ # perform
20
+ # @params params
21
+ #
22
+ # DashApi::Query is a helper module which parses URL parameters
23
+ # passed to a Rails Controller into attributes used to query a DashTable
24
+
25
+ def self.parse(params)
26
+
27
+ keywords = params[:keywords]
28
+
29
+ if params[:select]
30
+ select_fields = params[:select]&.split(',')
31
+ end
32
+
33
+ if params[:order]
34
+ sort_by, sort_direction = params[:order].split(DELIMITER)
35
+ sort_direction = "desc" if sort_direction and !SORT_DIRECTIONS.include?(sort_direction)
36
+ end
37
+
38
+ if params[:includes]
39
+ associations = params[:includes].split(",").map(&:strip)
40
+ end
41
+
42
+ filters = []
43
+ if params[:filters]
44
+ params[:filters].split(',').each do |filter_param|
45
+ field, rel, value = filter_param.split(DELIMITER)
46
+ rel = "eq" unless OPERATORS.keys.include?(rel.to_sym)
47
+ operator = OPERATORS[rel.to_sym] || '='
48
+ filters << {
49
+ field: field,
50
+ operator: operator,
51
+ value: value
52
+ }
53
+ end
54
+ end
55
+
56
+ page = params[:page]&.to_i || 1
57
+ per_page = params[:per_page]&.to_i || PER_PAGE
58
+
59
+ {
60
+ keywords: keywords,
61
+ select_fields: select_fields,
62
+ sort_by: sort_by,
63
+ sort_direction: sort_direction,
64
+ filters: filters,
65
+ page: page,
66
+ associations: associations,
67
+ per_page: per_page
68
+ }
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,57 @@
1
+ module DashApi
2
+ module Schema
3
+
4
+ EXCLUDED_TABLES = [
5
+ 'ar_internal_metadata',
6
+ 'schema_migrations'
7
+ ]
8
+
9
+ attr_accessor :tables
10
+
11
+ def self.table_names
12
+ @tables = ActiveRecord::Base.connection.tables
13
+ @tables.filter!{|t| !EXCLUDED_TABLES.include?(t)}.sort!
14
+ end
15
+
16
+ def self.table_schema(table_name)
17
+ @tables = table_names
18
+ dash_table = DashTable.modelize(table_name)
19
+ dash_table.reset_column_information
20
+ dash_table.columns.map{ |column|
21
+ render_column(column)
22
+ }
23
+ end
24
+
25
+ def self.db_schema
26
+ @tables = table_names
27
+ schema = {
28
+ tables: @tables
29
+ }
30
+ @tables.each do |table_name|
31
+ schema[table_name] = table_schema(table_name)
32
+ end
33
+ schema
34
+ end
35
+
36
+ def self.render_column(column)
37
+ {
38
+ name: column.name,
39
+ type: column.sql_type_metadata.type,
40
+ limit: column.sql_type_metadata.limit,
41
+ precision: column.sql_type_metadata.precision,
42
+ foreign_key: foreign_key?(column.name),
43
+ foreign_table: foreign_table(column.name)
44
+ }
45
+ end
46
+
47
+ def self.foreign_key?(column_name)
48
+ column_name[-2..-1]&.downcase === 'id' && column_name.downcase != 'id'
49
+ end
50
+
51
+ def self.foreign_table(column_name)
52
+ table_prefix = column_name[0...-3]
53
+ @tables.find{|t| t === table_prefix || t === table_prefix.pluralize }
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dash</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "dash/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,17 @@
1
+ DashApi::Engine.routes.draw do
2
+
3
+ root 'schema#index'
4
+
5
+ get 'schema' => 'schema#schema'
6
+ get 'schema/:table_name' => 'schema#show'
7
+
8
+ get '/:table_name' => 'api#index'
9
+ get '/:table_name/:id' => 'api#show'
10
+ put '/:table_name/:id' => 'api#update'
11
+ post '/:table_name' => 'api#create'
12
+ delete '/:table_name/:id' => 'api#destroy'
13
+
14
+ post '/:table_name/update_many' => 'api#update_many'
15
+ post '/:table_name/delete_many' => 'api#delete_many'
16
+
17
+ end
@@ -0,0 +1,5 @@
1
+ module DashApi
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace DashApi
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module DashApi
2
+ VERSION = '0.0.16'
3
+ end
data/lib/dash_api.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "dash_api/version"
2
+ require "dash_api/engine"
3
+
4
+ module DashApi
5
+
6
+ mattr_accessor :enable_auth
7
+ mattr_accessor :jwt_secret
8
+ mattr_accessor :jwt_algorithm
9
+
10
+ mattr_accessor :api_token
11
+
12
+ mattr_accessor :exclude_fields
13
+ mattr_accessor :exclude_tables
14
+
15
+
16
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :dash do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dash_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.16
5
+ platform: ruby
6
+ authors:
7
+ - Rami Bitar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.4
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.1.4.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.1.4
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.1.4.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: pg
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pg_search
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: kaminari
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: dotenv-rails
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: jwt
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ description: Dash is a Rails engine that auto-generates a REST API for your postgres
104
+ database.
105
+ email:
106
+ - rami@skillhire.com
107
+ executables: []
108
+ extensions: []
109
+ extra_rdoc_files: []
110
+ files:
111
+ - MIT-LICENSE
112
+ - README.md
113
+ - Rakefile
114
+ - app/assets/config/dash_manifest.js
115
+ - app/assets/stylesheets/dash/application.css
116
+ - app/controllers/dash_api/api_controller.rb
117
+ - app/controllers/dash_api/application_controller.rb
118
+ - app/controllers/dash_api/schema_controller.rb
119
+ - app/helpers/dash_api/application_helper.rb
120
+ - app/jobs/dash_api/application_job.rb
121
+ - app/mailers/dash_api/application_mailer.rb
122
+ - app/models/concerns/dash_api/dash_model.rb
123
+ - app/models/dash_api/application_record.rb
124
+ - app/models/dash_api/dash_table.rb
125
+ - app/services/dash_api/json_web_token.rb
126
+ - app/services/dash_api/query.rb
127
+ - app/services/dash_api/schema.rb
128
+ - app/views/layouts/dash_api/application.html.erb
129
+ - config/routes.rb
130
+ - lib/dash_api.rb
131
+ - lib/dash_api/engine.rb
132
+ - lib/dash_api/version.rb
133
+ - lib/tasks/dash_tasks.rake
134
+ homepage: https://github.com/skillhire/dash_api.git
135
+ licenses:
136
+ - MIT
137
+ metadata:
138
+ homepage_uri: https://github.com/skillhire/dash_api.git
139
+ source_code_uri: https://github.com/skillhire/dash_api.git
140
+ changelog_uri: https://github.com/skillhire/dash_api.git
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubygems_version: 3.1.6
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: REST API for your postgres database.
160
+ test_files: []