dash_api 0.0.16
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|