dash_api 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []