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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +135 -15
  3. data/Rakefile +1 -1
  4. data/app/assets/config/blazer_json_api_manifest.js +2 -2
  5. data/app/assets/javascripts/{blazer_json_api → blazer/api}/application.js +0 -0
  6. data/app/assets/stylesheets/{blazer_json_api → blazer/api}/application.css +0 -0
  7. data/app/controllers/blazer/api/application_controller.rb +17 -0
  8. data/app/controllers/blazer/api/queries_controller.rb +36 -0
  9. data/app/helpers/blazer/api/application_helper.rb +8 -0
  10. data/app/jobs/blazer/api/application_job.rb +8 -0
  11. data/app/mailers/blazer/api/application_mailer.rb +10 -0
  12. data/app/models/blazer/api/application_record.rb +9 -0
  13. data/app/views/layouts/blazer/api/application.html.erb +14 -0
  14. data/config/routes.rb +1 -1
  15. data/lib/blazer/api/config.rb +14 -0
  16. data/lib/blazer/api/engine.rb +9 -0
  17. data/lib/blazer/api/process_statement_variables.rb +55 -0
  18. data/lib/blazer/api/result_to_nested_json.rb +58 -0
  19. data/lib/blazer/api/version.rb +7 -0
  20. data/lib/blazer/json_api.rb +11 -0
  21. metadata +28 -28
  22. data/app/controllers/blazer_json_api/application_controller.rb +0 -15
  23. data/app/controllers/blazer_json_api/queries_controller.rb +0 -34
  24. data/app/helpers/blazer_json_api/application_helper.rb +0 -6
  25. data/app/jobs/blazer_json_api/application_job.rb +0 -6
  26. data/app/mailers/blazer_json_api/application_mailer.rb +0 -8
  27. data/app/models/blazer_json_api/application_record.rb +0 -7
  28. data/app/views/layouts/blazer_json_api/application.html.erb +0 -14
  29. data/lib/blazer_json_api/config.rb +0 -12
  30. data/lib/blazer_json_api/engine.rb +0 -7
  31. data/lib/blazer_json_api/process_statement_variables.rb +0 -55
  32. data/lib/blazer_json_api/result_to_nested_json.rb +0 -56
  33. data/lib/blazer_json_api/version.rb +0 -5
  34. data/lib/blazer_json_api.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f6334f046a09d7f2a4ab42a69c12658d08c8cfd8741b6dd970e1245b65c664c
4
- data.tar.gz: f7b09af286f45776b3801d0b2aff67f55c67bea066c7ea7a681708e5182973b6
3
+ metadata.gz: e325bb61490d8a0910e0952abd8a51236162e3f706d3f9fa61536e3a1096c0e6
4
+ data.tar.gz: 95b2f4209f73d76db0b0efcbd298e07c2c1240df2c6cccaef75207443281da74
5
5
  SHA512:
6
- metadata.gz: 3409b404960bd3c197aa03731653f559e6eca6c2f38a732c98318c0a8eefe410a2ba13c09b9d2d2bd6111c9437f8105b486c22c5b6e8d68aff7d32c7320ef214
7
- data.tar.gz: 114d9d8e40a4c2205b6bb2f0ff94a0c2113d8342cffbb91f64fa1946e61156d3a8ad92656364ce02a822dd98a2b3786a13284778316d67da764928ac0b4919d1
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 built on Blazers query variables meaning the APIs can be highly dynamic and flexible
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 datasources meaning you can potentially build APIs that access beyond the applications database (e.g. ElasticSearch, Google BigQuery, Salesforce)
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 BlazerJsonAPI, at: 'blazer-api'
30
+ mount Blazer::Api::Engine, at: 'blazer-api'
30
31
  ```
31
32
 
32
- Configure authentication in an initializer as follows (e.g. in `initializers/blazer_json_api.rb`)
33
+ Configure authentication in an initializer as follows (e.g. in `initializers/blazer_api.rb`)
33
34
 
34
35
  ```ruby
35
- BlazerJsonAPI::Config.username = <api-username>
36
- BlazerJsonAPI::Config.password = <api-password>
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
- > TODO
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
- ## Contributing
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
- > TODO
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 = 'BlazerJsonApi'
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/blazer_json_api .js
2
- //= link_directory ../stylesheets/blazer_json_api .css
1
+ //= link_directory ../javascripts/blazer/api .js
2
+ //= link_directory ../stylesheets/blazer/api .css
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ module ApplicationHelper
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ class ApplicationJob < ActiveJob::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ class ApplicationMailer < ActionMailer::Base
6
+ default from: 'from@example.com'
7
+ layout 'mailer'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ class ApplicationRecord < ActiveRecord::Base
6
+ self.abstract_class = true
7
+ end
8
+ end
9
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- BlazerJsonAPI::Engine.routes.draw do
3
+ Blazer::Api::Engine.routes.draw do
4
4
  resources :queries, only: :show
5
5
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ class Config
6
+ # defaults
7
+ @@nesting_column_separator = '__'
8
+
9
+ cattr_accessor :username
10
+ cattr_accessor :password
11
+ cattr_accessor :nesting_column_separator
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Blazer::Api
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blazer
4
+ module Api
5
+ VERSION = '0.1.1e'
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'blazer/api/engine'
4
+ require 'blazer/api/config'
5
+ require 'blazer/api/process_statement_variables'
6
+ require 'blazer/api/result_to_nested_json'
7
+
8
+ module Blazer
9
+ module Api
10
+ end
11
+ 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.1d
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-02 00:00:00.000000000 Z
11
+ date: 2021-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: railties
14
+ name: blazer
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
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: '5'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: blazer
28
+ name: railties
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '2.0'
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: '2.0'
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/blazer_json_api/application.js
68
- - app/assets/stylesheets/blazer_json_api/application.css
69
- - app/controllers/blazer_json_api/application_controller.rb
70
- - app/controllers/blazer_json_api/queries_controller.rb
71
- - app/helpers/blazer_json_api/application_helper.rb
72
- - app/jobs/blazer_json_api/application_job.rb
73
- - app/mailers/blazer_json_api/application_mailer.rb
74
- - app/models/blazer_json_api/application_record.rb
75
- - app/views/layouts/blazer_json_api/application.html.erb
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/blazer_json_api.rb
78
- - lib/blazer_json_api/config.rb
79
- - lib/blazer_json_api/engine.rb
80
- - lib/blazer_json_api/process_statement_variables.rb
81
- - lib/blazer_json_api/result_to_nested_json.rb
82
- - lib/blazer_json_api/version.rb
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.4'
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,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonApi
4
- module ApplicationHelper
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonApi
4
- class ApplicationJob < ActiveJob::Base
5
- end
6
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonApi
4
- class ApplicationMailer < ActionMailer::Base
5
- default from: 'from@example.com'
6
- layout 'mailer'
7
- end
8
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonApi
4
- class ApplicationRecord < ActiveRecord::Base
5
- self.abstract_class = true
6
- end
7
- 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,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonAPI
4
- class Config
5
- # defaults
6
- @@nesting_column_separator = '__'
7
-
8
- cattr_accessor :username
9
- cattr_accessor :password
10
- cattr_accessor :nesting_column_separator
11
- end
12
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonAPI
4
- class Engine < ::Rails::Engine
5
- isolate_namespace BlazerJsonAPI
6
- end
7
- end
@@ -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
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BlazerJsonAPI
4
- VERSION = '0.1.1d'
5
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'blazer_json_api/engine'
4
- require 'blazer_json_api/config'
5
- require 'blazer_json_api/process_statement_variables'
6
- require 'blazer_json_api/result_to_nested_json'
7
-
8
- module BlazerJsonAPI
9
- end