blazer_json_api 0.1.1d → 0.1.1e
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +135 -15
- data/Rakefile +1 -1
- data/app/assets/config/blazer_json_api_manifest.js +2 -2
- data/app/assets/javascripts/{blazer_json_api → blazer/api}/application.js +0 -0
- data/app/assets/stylesheets/{blazer_json_api → blazer/api}/application.css +0 -0
- data/app/controllers/blazer/api/application_controller.rb +17 -0
- data/app/controllers/blazer/api/queries_controller.rb +36 -0
- data/app/helpers/blazer/api/application_helper.rb +8 -0
- data/app/jobs/blazer/api/application_job.rb +8 -0
- data/app/mailers/blazer/api/application_mailer.rb +10 -0
- data/app/models/blazer/api/application_record.rb +9 -0
- data/app/views/layouts/blazer/api/application.html.erb +14 -0
- data/config/routes.rb +1 -1
- data/lib/blazer/api/config.rb +14 -0
- data/lib/blazer/api/engine.rb +9 -0
- data/lib/blazer/api/process_statement_variables.rb +55 -0
- data/lib/blazer/api/result_to_nested_json.rb +58 -0
- data/lib/blazer/api/version.rb +7 -0
- data/lib/blazer/json_api.rb +11 -0
- metadata +28 -28
- data/app/controllers/blazer_json_api/application_controller.rb +0 -15
- data/app/controllers/blazer_json_api/queries_controller.rb +0 -34
- data/app/helpers/blazer_json_api/application_helper.rb +0 -6
- data/app/jobs/blazer_json_api/application_job.rb +0 -6
- data/app/mailers/blazer_json_api/application_mailer.rb +0 -8
- data/app/models/blazer_json_api/application_record.rb +0 -7
- data/app/views/layouts/blazer_json_api/application.html.erb +0 -14
- data/lib/blazer_json_api/config.rb +0 -12
- data/lib/blazer_json_api/engine.rb +0 -7
- data/lib/blazer_json_api/process_statement_variables.rb +0 -55
- data/lib/blazer_json_api/result_to_nested_json.rb +0 -56
- data/lib/blazer_json_api/version.rb +0 -5
- data/lib/blazer_json_api.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e325bb61490d8a0910e0952abd8a51236162e3f706d3f9fa61536e3a1096c0e6
|
4
|
+
data.tar.gz: 95b2f4209f73d76db0b0efcbd298e07c2c1240df2c6cccaef75207443281da74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 576b3fcce73e53db19fe1a4381ca94e439061f50406ffe198ecd5f67c1bd37a401af0ae16c6efde9111a4fb290c53d2dfe14964ef1d48bad33859aca1dcbd013
|
7
|
+
data.tar.gz: 0be572b3cf7d73ea051e2c20f896f7449f498fb1e39063bbc9a1fb2af738ba0c0798991a75a06f6eed7e48e1be90e829c4dbf20fc9d0ee335583329c4363b014
|
data/README.md
CHANGED
@@ -2,16 +2,17 @@
|
|
2
2
|
An extension to [Blazer](https://github.com/ankane/blazer) to enable exposing your queries as JSON via API so it can be consumed outside of Blazer by your application.
|
3
3
|
|
4
4
|
## Features
|
5
|
-
- **Powered by SQL** Author APIs quickly using Blazers IDE. Particular useful for private/internal APIs that fall outside of your standard API endpoints
|
6
|
-
- **No deploy APIs** Expermental APIs can be authored quickly via Blazer without the need to do a deploy
|
7
|
-
- **Flexible structure** JSON response structure can be controlled directly in SQL by using a column naming convention (double underscore denotes a nesting by default, but can be overridden)
|
8
|
-
- **Security** You'll likely want to lock down API access so APIs are authenticated separately to standard Blazer authentication using HTTP basic authentication to avoid granding everyone with access to Blazer also access to your APIs.
|
9
|
-
- **URL parameters** URL parameters are also supported
|
5
|
+
- **Powered by SQL** Author APIs quickly using Blazers SQL based IDE. Particular useful for private/internal APIs that fall outside of your standard API endpoints
|
6
|
+
- **No deploy APIs** Expermental APIs can be authored and iterated on quickly via Blazer without the need to do a deploy.
|
7
|
+
- **Flexible structure** JSON response structure can be controlled directly in SQL by using a column naming convention (double underscore `__` denotes a nesting by default, but can be overridden)
|
8
|
+
- **Security** You'll likely want to lock down API access so APIs are authenticated separately to the standard Blazer auth model, so authentication is enabled using HTTP basic authentication to avoid granding everyone with access to Blazer also access to your APIs.
|
9
|
+
- **URL parameters** URL parameters are also supported via Blazers query variables meaning the APIs can be highly dynamic and flexible
|
10
10
|
- **Pagination** Pagination can be controlled using query variables in combination with limits and offsets
|
11
|
-
- **Multiple data sources** Blazer supports multiple
|
11
|
+
- **Multiple data sources** Blazer supports multiple data sources meaning you can potentially build APIs that access beyond the applications database (e.g. ElasticSearch, Google BigQuery, Salesforce)
|
12
|
+
- **Permissions** Use Blazers [basic permissions mode](https://github.com/ankane/blazer#query-permissions) with your own naming conventions to control access to APIs based queries.
|
12
13
|
|
13
14
|
## Installation
|
14
|
-
Follow the installation steps described to get Blazer up and running.
|
15
|
+
Follow the installation steps described to get [Blazer](https://github.com/ankane/blazer#installation) up and running.
|
15
16
|
Then, add this line to your application's Gemfile:
|
16
17
|
|
17
18
|
```ruby
|
@@ -26,30 +27,149 @@ $ bundle
|
|
26
27
|
And mount the engine in your `config/routes.rb`:
|
27
28
|
|
28
29
|
```ruby
|
29
|
-
mount
|
30
|
+
mount Blazer::Api::Engine, at: 'blazer-api'
|
30
31
|
```
|
31
32
|
|
32
|
-
Configure authentication in an initializer as follows (e.g. in `initializers/
|
33
|
+
Configure authentication in an initializer as follows (e.g. in `initializers/blazer_api.rb`)
|
33
34
|
|
34
35
|
```ruby
|
35
|
-
|
36
|
-
|
36
|
+
Blazer::Api::Config.username = <api-username>
|
37
|
+
Blazer::Api::Config.password = <api-password>
|
37
38
|
```
|
38
39
|
|
39
40
|
## Usage
|
40
41
|
Create queries as normal via Blazer and use the query identifier to render the JSON via the mounted location.
|
41
42
|
|
42
|
-
e.g. `/blazer-api/queries/1-all-users`
|
43
|
+
e.g. `/blazer-api/queries/1-all-users` or `/blazer-api/queries/1`
|
44
|
+
URL params can be added where necessary also
|
45
|
+
e.g. `/blazer-api/queries/1-all-users?page=1&per_page=30`
|
43
46
|
|
44
47
|
### Example queries
|
45
48
|
|
49
|
+
#### A simple index like request
|
50
|
+
Fetching specifics of all users as follows:
|
46
51
|
|
47
|
-
|
52
|
+
```sql
|
53
|
+
SELECT id, username, first_name, last_name, email, country
|
54
|
+
FROM users
|
55
|
+
```
|
56
|
+
This would result in the following API response
|
57
|
+
```json
|
58
|
+
[
|
59
|
+
{
|
60
|
+
"id":1,
|
61
|
+
"username":"blazer_tommy",
|
62
|
+
"first_name":"Tom",
|
63
|
+
"last_name":"Carey",
|
64
|
+
"email":"tom.carey@gmail.com",
|
65
|
+
"country":"Ireland"
|
66
|
+
},
|
67
|
+
{
|
68
|
+
"id":2,
|
69
|
+
"username":"blazer_john",
|
70
|
+
"first_name":"John",
|
71
|
+
"last_name":"Doyle",
|
72
|
+
"email":"john.doyle@gmail.com",
|
73
|
+
"country":"USA"
|
74
|
+
}
|
75
|
+
// ...
|
76
|
+
]
|
77
|
+
```
|
78
|
+
#### A simple single resource GET request using a variable
|
48
79
|
|
49
|
-
|
80
|
+
Using a variable, a specific resource can be fetched.
|
81
|
+
Note: the use of `LIMIT 1` can be used to be explicit in desiring a single record in the response, as opposed to a collection
|
82
|
+
|
83
|
+
```sql
|
84
|
+
SELECT id, username, first_name, last_name, email, country
|
85
|
+
FROM users
|
86
|
+
WHERE username={username}
|
87
|
+
LIMIT 1
|
88
|
+
```
|
89
|
+
Now, the username can be passed as a URL parameter to the API to fetch the relevant record.
|
90
|
+
It would result in the following response.
|
91
|
+
```json
|
92
|
+
{
|
93
|
+
"id":2,
|
94
|
+
"username":"blazer_john",
|
95
|
+
"first_name":"John",
|
96
|
+
"last_name":"Doyle",
|
97
|
+
"email":"john.doyle@gmail.com",
|
98
|
+
"country":"USA"
|
99
|
+
}
|
100
|
+
```
|
101
|
+
#### Controlling response structure
|
102
|
+
Standard queries return flat JSON responses that correspond to the results table from executing the SQL.
|
103
|
+
It's possible to control the JSON structure by using double underscores to denote the desired nesting
|
104
|
+
|
105
|
+
Take, for example, the following query:
|
50
106
|
|
107
|
+
```sql
|
108
|
+
SELECT users.id, username, first_name, last_name, teams.name as team__name, teams.location as team__location, email
|
109
|
+
FROM users
|
110
|
+
JOIN users ON users.team_id = teams.id
|
111
|
+
```
|
112
|
+
|
113
|
+
Would result in the following structure in the response:
|
114
|
+
|
115
|
+
```json
|
116
|
+
[
|
117
|
+
{
|
118
|
+
"id":1,
|
119
|
+
"username":"blazer_tommy",
|
120
|
+
"first_name":"Tom",
|
121
|
+
"last_name":"Carey",
|
122
|
+
"team":{
|
123
|
+
"name":"defenders",
|
124
|
+
"location":"Dublin"
|
125
|
+
},
|
126
|
+
"email":"tom.carey@gmail.com"
|
127
|
+
},
|
128
|
+
{
|
129
|
+
"id":2,
|
130
|
+
"username":"blazer_john",
|
131
|
+
"first_name":"John",
|
132
|
+
"last_name":"Doyle",
|
133
|
+
"team":{
|
134
|
+
"name":"responders",
|
135
|
+
"location":"London"
|
136
|
+
},
|
137
|
+
"email":"john.doyle@gmail.com"
|
138
|
+
}
|
139
|
+
]
|
140
|
+
```
|
141
|
+
Deeper nesting is also possible, just continue the pattern e.g. `a__deeper__nested__value`
|
142
|
+
|
143
|
+
#### Paginating potentially large responses
|
144
|
+
If your query could return a large response, it's generally a good idea to paginate it.
|
145
|
+
Pagination can be achieved in many ways, but a basic example can be done as follows using a combination
|
146
|
+
of variables in the query and `LIMIT` and `OFFSET`.
|
147
|
+
|
148
|
+
```sql
|
149
|
+
SELECT id, username, first_name, last_name, email, country
|
150
|
+
FROM users
|
151
|
+
LIMIT {per_page}
|
152
|
+
OFFSET ({page}-1)*{per_page}
|
153
|
+
```
|
154
|
+
Using this technique, URL params can be used by the requester to control pagination.
|
155
|
+
In this example, `page` corresponds to the desired page in the paginated collection and `per_page` corresponds to the desired size of records in each page
|
156
|
+
|
157
|
+
This technique can be used in combination with some default settings for these parameters in blazers config file `blazer.yml`.
|
158
|
+
Having defaults means if they are not specified by the requester, the defaults will automatically be applied.
|
159
|
+
```yaml
|
160
|
+
variable_defaults:
|
161
|
+
# pagination defaults
|
162
|
+
per_page: 30
|
163
|
+
page: 1
|
164
|
+
```
|
165
|
+
|
166
|
+
## Contributing
|
167
|
+
Want to improve this library, please do!
|
51
168
|
|
52
|
-
|
169
|
+
* Report bugs
|
170
|
+
* Fix bugs and submit pull requests
|
171
|
+
* Write, clarify, or fix documentation
|
172
|
+
* Suggest or add new features
|
53
173
|
|
54
174
|
## License
|
55
175
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -10,7 +10,7 @@ require 'rdoc/task'
|
|
10
10
|
|
11
11
|
RDoc::Task.new(:rdoc) do |rdoc|
|
12
12
|
rdoc.rdoc_dir = 'rdoc'
|
13
|
-
rdoc.title = '
|
13
|
+
rdoc.title = 'Blazer::Api'
|
14
14
|
rdoc.options << '--line-numbers'
|
15
15
|
rdoc.rdoc_files.include('README.md')
|
16
16
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
@@ -1,2 +1,2 @@
|
|
1
|
-
//= link_directory ../javascripts/
|
2
|
-
//= link_directory ../stylesheets/
|
1
|
+
//= link_directory ../javascripts/blazer/api .js
|
2
|
+
//= link_directory ../stylesheets/blazer/api .css
|
File without changes
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blazer
|
4
|
+
module Api
|
5
|
+
class ApplicationController < ActionController::Base
|
6
|
+
protect_from_forgery with: :exception
|
7
|
+
|
8
|
+
def record_not_found
|
9
|
+
render json: [], status: :not_found
|
10
|
+
end
|
11
|
+
|
12
|
+
def render_errors(error_messages)
|
13
|
+
render json: { errors: error_messages }, status: :bad_request
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blazer
|
4
|
+
module Api
|
5
|
+
class QueriesController < ApplicationController
|
6
|
+
if Blazer::Api::Config.username && Blazer::Api::Config.password
|
7
|
+
http_basic_authenticate_with name: Blazer::Api::Config.username, password: Blazer::Api::Config.password
|
8
|
+
end
|
9
|
+
before_action :set_query
|
10
|
+
|
11
|
+
def show
|
12
|
+
@statement = @query.statement
|
13
|
+
data_source = @query.data_source
|
14
|
+
process_variables(@statement, data_source)
|
15
|
+
result = Blazer.data_sources[data_source].run_statement(@statement)
|
16
|
+
|
17
|
+
if result.error.present?
|
18
|
+
render_errors(Array(result.error))
|
19
|
+
else
|
20
|
+
render json: Blazer::Api::ResultToNestedJson.new(@statement, result).call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def set_query
|
27
|
+
@query = Blazer::Query.find_by(id: params[:id].to_s.split('-').first)
|
28
|
+
record_not_found && return if @query.blank?
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_variables(statement, data_source)
|
32
|
+
Blazer::Api::ProcessStatementVariables.new(statement, data_source, params).call
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Blazer json api</title>
|
5
|
+
<%= stylesheet_link_tag "blazer/api/application", media: "all" %>
|
6
|
+
<%= javascript_include_tag "blazer/api/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
CHANGED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Taken from: https://github.com/ankane/blazer/blob/ac7e02e0fe19ee6c0bc8c7f3ba87be0d260f8255/app/controllers/blazer/base_controller.rb#L35
|
4
|
+
# Necessary for processing any query variables safely
|
5
|
+
# Refactored to avoid rubocop breaches
|
6
|
+
module Blazer
|
7
|
+
module Api
|
8
|
+
class ProcessStatementVariables
|
9
|
+
attr_reader :statement, :data_source, :params
|
10
|
+
|
11
|
+
def initialize(statement, data_source, params)
|
12
|
+
@statement = statement
|
13
|
+
@data_source = data_source
|
14
|
+
@params = params
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
return unless bind_variables.all? { |v| params[v] }
|
19
|
+
|
20
|
+
bind_variables.each do |variable|
|
21
|
+
value = params[variable].presence
|
22
|
+
if value
|
23
|
+
if variable.end_with?('_at')
|
24
|
+
begin
|
25
|
+
value = Blazer.time_zone.parse(value)
|
26
|
+
rescue StandardError
|
27
|
+
# do nothing
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if /\A\d+\z/.match?(value.to_s)
|
32
|
+
value = value.to_i
|
33
|
+
elsif /\A\d+\.\d+\z/.match?(value.to_s)
|
34
|
+
value = value.to_f
|
35
|
+
end
|
36
|
+
end
|
37
|
+
value = Blazer.transform_variable.call(variable, value) if Blazer.transform_variable
|
38
|
+
statement.gsub!("{#{variable}}", ActiveRecord::Base.connection.quote(value))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def bind_variables
|
45
|
+
@bind_variables ||=
|
46
|
+
begin
|
47
|
+
(bind_variables ||= []).concat(Blazer.extract_vars(statement)).uniq!
|
48
|
+
bind_variables.each do |variable|
|
49
|
+
params[variable] ||= Blazer.data_sources[data_source].variable_defaults[variable]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Converts a blazer query result table into nested JSON
|
4
|
+
# Used the convention of a double underscore in the column header to denote a parent key or nesting
|
5
|
+
# e.g. { 'parent__child': '1' } becomes { 'parent': { 'child': 1 }}
|
6
|
+
module Blazer
|
7
|
+
module Api
|
8
|
+
class ResultToNestedJson
|
9
|
+
delegate :nesting_column_separator, to: Blazer::Api::Config
|
10
|
+
|
11
|
+
attr_reader :statement, :blazer_result
|
12
|
+
|
13
|
+
def initialize(statement, blazer_result)
|
14
|
+
@statement = statement
|
15
|
+
@blazer_result = blazer_result
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
transformed_result =
|
20
|
+
blazer_result_to_json(blazer_result).each do |hash|
|
21
|
+
hash.keys.select { |key| key =~ /#{nesting_column_separator}/ }.each do |namespaced_key|
|
22
|
+
nested_keys = namespaced_key.to_s.split(nesting_column_separator)
|
23
|
+
hash.deep_merge!(deep_hash_set(*nested_keys[0..nested_keys.size], hash[namespaced_key]))
|
24
|
+
hash.delete(namespaced_key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
collection_or_single_record(transformed_result)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def blazer_result_to_json(blazer_result)
|
33
|
+
blazer_result.rows.map do |row|
|
34
|
+
row_hash = {}
|
35
|
+
row.each_with_index do |value, value_index|
|
36
|
+
row_hash[blazer_result.columns[value_index]] = value
|
37
|
+
end
|
38
|
+
row_hash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# recursively sets a nested key in a hash (like the opposite to Hash.dig)
|
43
|
+
# e.g. deep_hash_set(*['a', 'b' , 'c'], 4)
|
44
|
+
# { a => { b => { c => 4 } } }
|
45
|
+
# @return Hash
|
46
|
+
def deep_hash_set(*keys, value)
|
47
|
+
keys.empty? ? value : { keys.first => deep_hash_set(*keys.drop(1), value) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Use the presence of LIMIT 1 in the query to decide whether to render
|
51
|
+
# a collection response (like an index)
|
52
|
+
# or a single entry response
|
53
|
+
def collection_or_single_record(result)
|
54
|
+
/LIMIT 1$/i.match?(statement) ? result.first : result
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blazer_json_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1e
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Farrell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-11-
|
11
|
+
date: 2021-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: blazer
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: railties
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec-rails
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,22 +64,22 @@ files:
|
|
64
64
|
- README.md
|
65
65
|
- Rakefile
|
66
66
|
- app/assets/config/blazer_json_api_manifest.js
|
67
|
-
- app/assets/javascripts/
|
68
|
-
- app/assets/stylesheets/
|
69
|
-
- app/controllers/
|
70
|
-
- app/controllers/
|
71
|
-
- app/helpers/
|
72
|
-
- app/jobs/
|
73
|
-
- app/mailers/
|
74
|
-
- app/models/
|
75
|
-
- app/views/layouts/
|
67
|
+
- app/assets/javascripts/blazer/api/application.js
|
68
|
+
- app/assets/stylesheets/blazer/api/application.css
|
69
|
+
- app/controllers/blazer/api/application_controller.rb
|
70
|
+
- app/controllers/blazer/api/queries_controller.rb
|
71
|
+
- app/helpers/blazer/api/application_helper.rb
|
72
|
+
- app/jobs/blazer/api/application_job.rb
|
73
|
+
- app/mailers/blazer/api/application_mailer.rb
|
74
|
+
- app/models/blazer/api/application_record.rb
|
75
|
+
- app/views/layouts/blazer/api/application.html.erb
|
76
76
|
- config/routes.rb
|
77
|
-
- lib/
|
78
|
-
- lib/
|
79
|
-
- lib/
|
80
|
-
- lib/
|
81
|
-
- lib/
|
82
|
-
- lib/
|
77
|
+
- lib/blazer/api/config.rb
|
78
|
+
- lib/blazer/api/engine.rb
|
79
|
+
- lib/blazer/api/process_statement_variables.rb
|
80
|
+
- lib/blazer/api/result_to_nested_json.rb
|
81
|
+
- lib/blazer/api/version.rb
|
82
|
+
- lib/blazer/json_api.rb
|
83
83
|
- lib/tasks/blazer_json_api_tasks.rake
|
84
84
|
homepage: https://github.com/johnmfarrell1/blazer_json_api
|
85
85
|
licenses:
|
@@ -93,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '2.
|
96
|
+
version: '2.5'
|
97
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
98
|
requirements:
|
99
99
|
- - ">"
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module BlazerJsonApi
|
4
|
-
class ApplicationController < ActionController::Base
|
5
|
-
protect_from_forgery with: :exception
|
6
|
-
|
7
|
-
def record_not_found
|
8
|
-
render json: [], status: :not_found
|
9
|
-
end
|
10
|
-
|
11
|
-
def render_errors(error_messages)
|
12
|
-
render json: { errors: error_messages }, status: :bad_request
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module BlazerJsonApi
|
4
|
-
class QueriesController < ApplicationController
|
5
|
-
if BlazerJsonAPI::Config.username && BlazerJsonAPI::Config.password
|
6
|
-
http_basic_authenticate_with name: BlazerJsonAPI::Config.username, password: BlazerJsonAPI::Config.password
|
7
|
-
end
|
8
|
-
before_action :set_query
|
9
|
-
|
10
|
-
def show
|
11
|
-
@statement = @query.statement
|
12
|
-
data_source = @query.data_source
|
13
|
-
process_variables(@statement, data_source)
|
14
|
-
result = Blazer.data_sources[data_source].run_statement(@statement)
|
15
|
-
|
16
|
-
if result.error.present?
|
17
|
-
render_errors(Array(result.error))
|
18
|
-
else
|
19
|
-
render json: BlazerJsonAPI::ResultToNestedJson.new(@statement, result).call
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def set_query
|
26
|
-
@query = Blazer::Query.find_by(id: params[:id].to_s.split('-').first)
|
27
|
-
record_not_found && return if @query.blank?
|
28
|
-
end
|
29
|
-
|
30
|
-
def process_variables(statement, data_source)
|
31
|
-
BlazerJsonAPI::ProcessStatementVariables.new(statement, data_source, params).call
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html>
|
3
|
-
<head>
|
4
|
-
<title>Blazer json api</title>
|
5
|
-
<%= stylesheet_link_tag "blazer_json_api/application", media: "all" %>
|
6
|
-
<%= javascript_include_tag "blazer_json_api/application" %>
|
7
|
-
<%= csrf_meta_tags %>
|
8
|
-
</head>
|
9
|
-
<body>
|
10
|
-
|
11
|
-
<%= yield %>
|
12
|
-
|
13
|
-
</body>
|
14
|
-
</html>
|
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Taken from: https://github.com/ankane/blazer/blob/ac7e02e0fe19ee6c0bc8c7f3ba87be0d260f8255/app/controllers/blazer/base_controller.rb#L35
|
4
|
-
# Necessary for processing any query variables safely
|
5
|
-
# Refactored to avoid rubocop breaches
|
6
|
-
module BlazerJsonAPI
|
7
|
-
class ProcessStatementVariables
|
8
|
-
attr_reader :statement, :data_source, :params
|
9
|
-
|
10
|
-
def initialize(statement, data_source, params)
|
11
|
-
@statement = statement
|
12
|
-
@data_source = data_source
|
13
|
-
@params = params
|
14
|
-
end
|
15
|
-
|
16
|
-
def call
|
17
|
-
return unless bind_variables.all? { |v| params[v] }
|
18
|
-
|
19
|
-
bind_variables.each do |variable|
|
20
|
-
value = params[variable].presence
|
21
|
-
if value
|
22
|
-
if variable.end_with?('_at')
|
23
|
-
begin
|
24
|
-
value = Blazer.time_zone.parse(value)
|
25
|
-
rescue StandardError
|
26
|
-
# do nothing
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
if /\A\d+\z/.match?(value.to_s)
|
31
|
-
value = value.to_i
|
32
|
-
elsif /\A\d+\.\d+\z/.match?(value.to_s)
|
33
|
-
value = value.to_f
|
34
|
-
end
|
35
|
-
end
|
36
|
-
if Blazer.transform_variable
|
37
|
-
value = Blazer.transform_variable.call(variable, value)
|
38
|
-
end
|
39
|
-
statement.gsub!("{#{variable}}", ActiveRecord::Base.connection.quote(value))
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def bind_variables
|
46
|
-
@bind_variables ||=
|
47
|
-
begin
|
48
|
-
(bind_variables ||= []).concat(Blazer.extract_vars(statement)).uniq!
|
49
|
-
bind_variables.each do |variable|
|
50
|
-
params[variable] ||= Blazer.data_sources[data_source].variable_defaults[variable]
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Converts a blazer query result table into nested JSON
|
4
|
-
# Used the convention of a double underscore in the column header to denote a parent key or nesting
|
5
|
-
# e.g. { 'parent__child': '1' } becomes { 'parent': { 'child': 1 }}
|
6
|
-
module BlazerJsonAPI
|
7
|
-
class ResultToNestedJson
|
8
|
-
delegate :nesting_column_separator, to: BlazerJsonAPI::Config
|
9
|
-
|
10
|
-
attr_reader :statement, :blazer_result
|
11
|
-
|
12
|
-
def initialize(statement, blazer_result)
|
13
|
-
@statement = statement
|
14
|
-
@blazer_result = blazer_result
|
15
|
-
end
|
16
|
-
|
17
|
-
def call
|
18
|
-
transformed_result =
|
19
|
-
blazer_result_to_json(blazer_result).each do |hash|
|
20
|
-
hash.keys.select { |key| key =~ /#{nesting_column_separator}/ }.each do |namespaced_key|
|
21
|
-
nested_keys = namespaced_key.to_s.split(nesting_column_separator)
|
22
|
-
hash.deep_merge!(deep_hash_set(*nested_keys[0..nested_keys.size], hash[namespaced_key]))
|
23
|
-
hash.delete(namespaced_key)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
collection_or_single_record(transformed_result)
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def blazer_result_to_json(blazer_result)
|
32
|
-
blazer_result.rows.map do |row|
|
33
|
-
row_hash = {}
|
34
|
-
row.each_with_index do |value, value_index|
|
35
|
-
row_hash[blazer_result.columns[value_index]] = value
|
36
|
-
end
|
37
|
-
row_hash
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# recursively sets a nested key in a hash (like the opposite to Hash.dig)
|
42
|
-
# e.g. deep_hash_set(*['a', 'b' , 'c'], 4)
|
43
|
-
# { a => { b => { c => 4 } } }
|
44
|
-
# @return Hash
|
45
|
-
def deep_hash_set(*keys, value)
|
46
|
-
keys.empty? ? value : { keys.first => deep_hash_set(*keys.drop(1), value) }
|
47
|
-
end
|
48
|
-
|
49
|
-
# Use the presence of LIMIT 1 in the query to decide whether to render
|
50
|
-
# a collection response (like an index)
|
51
|
-
# or a single entry response
|
52
|
-
def collection_or_single_record(result)
|
53
|
-
/LIMIT 1$/i.match?(statement) ? result.first : result
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
data/lib/blazer_json_api.rb
DELETED