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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +406 -0
- data/Rakefile +8 -0
- data/app/assets/config/dash_manifest.js +1 -0
- data/app/assets/stylesheets/dash/application.css +15 -0
- data/app/controllers/dash_api/api_controller.rb +89 -0
- data/app/controllers/dash_api/application_controller.rb +42 -0
- data/app/controllers/dash_api/schema_controller.rb +22 -0
- data/app/helpers/dash_api/application_helper.rb +4 -0
- data/app/jobs/dash_api/application_job.rb +4 -0
- data/app/mailers/dash_api/application_mailer.rb +6 -0
- data/app/models/concerns/dash_api/dash_model.rb +203 -0
- data/app/models/dash_api/application_record.rb +5 -0
- data/app/models/dash_api/dash_table.rb +9 -0
- data/app/services/dash_api/json_web_token.rb +19 -0
- data/app/services/dash_api/query.rb +72 -0
- data/app/services/dash_api/schema.rb +57 -0
- data/app/views/layouts/dash_api/application.html.erb +15 -0
- data/config/routes.rb +17 -0
- data/lib/dash_api/engine.rb +5 -0
- data/lib/dash_api/version.rb +3 -0
- data/lib/dash_api.rb +16 -0
- data/lib/tasks/dash_tasks.rake +4 -0
- metadata +160 -0
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 @@
|
|
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,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,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
|
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
|
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
|
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: []
|