blazer_json_api 0.1.1i → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -39
  3. data/Rakefile +1 -1
  4. data/app/assets/config/blazer_json_api_manifest.js +2 -2
  5. data/app/assets/javascripts/{blazer/api → blazer_json_api}/application.js +0 -0
  6. data/app/assets/stylesheets/{blazer/api → blazer_json_api}/application.css +0 -0
  7. data/app/controllers/blazer_json_api/application_controller.rb +20 -0
  8. data/app/controllers/blazer_json_api/queries_controller.rb +31 -0
  9. data/app/helpers/blazer_json_api/application_helper.rb +6 -0
  10. data/app/jobs/blazer_json_api/application_job.rb +6 -0
  11. data/app/mailers/blazer_json_api/application_mailer.rb +8 -0
  12. data/app/models/blazer_json_api/application_record.rb +7 -0
  13. data/app/views/layouts/blazer_json_api/application.html.erb +14 -0
  14. data/config/routes.rb +1 -1
  15. data/lib/blazer_json_api/configuration.rb +34 -0
  16. data/lib/blazer_json_api/engine.rb +11 -0
  17. data/lib/blazer_json_api/process_statement_variables.rb +53 -0
  18. data/lib/blazer_json_api/result_to_nested_json.rb +56 -0
  19. data/lib/blazer_json_api/version.rb +5 -0
  20. data/lib/blazer_json_api.rb +10 -0
  21. data/spec/rails_helper.rb +70 -0
  22. data/spec/spec_helper.rb +100 -0
  23. metadata +40 -36
  24. data/app/controllers/blazer/api/application_controller.rb +0 -17
  25. data/app/controllers/blazer/api/queries_controller.rb +0 -36
  26. data/app/helpers/blazer/api/application_helper.rb +0 -8
  27. data/app/jobs/blazer/api/application_job.rb +0 -8
  28. data/app/mailers/blazer/api/application_mailer.rb +0 -10
  29. data/app/models/blazer/api/application_record.rb +0 -9
  30. data/app/views/layouts/blazer/api/application.html.erb +0 -14
  31. data/lib/blazer/api/config.rb +0 -14
  32. data/lib/blazer/api/engine.rb +0 -9
  33. data/lib/blazer/api/process_statement_variables.rb +0 -55
  34. data/lib/blazer/api/result_to_nested_json.rb +0 -58
  35. data/lib/blazer/api/version.rb +0 -7
  36. data/lib/blazer/api.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07fd9c9b0b687bb830f8fe5bf216282965a5e0bb9c2390a9cc05e9eb56d22e4b
4
- data.tar.gz: 2c1d60956fbb7637d68c652764a50b39934ed7f2b9b080a95b0cfe9ec06a7b7b
3
+ metadata.gz: b6fe3871a2a73e0d9ca277c700e7ec943bb20e1bc99efb484419d7b9df31207b
4
+ data.tar.gz: 179dddb823192971bd0c44e4ef6d69c1055a64814567daa5fb4e6044f1c303e1
5
5
  SHA512:
6
- metadata.gz: 3f791140000106082ba74cddb0d0aefc81f2e4184e51572effa03501ed4cedf399706d1a94c6e994ad1975ee52e901e67ad6de758ea4f324964ff542ddf06b1d
7
- data.tar.gz: 9ab2a52d7ee5f1df8652c52dd238eb04937360f65977cbdada0893d377bb9ac4685917f8508b42cb4e15b0b9bf6b9e559465408504504eac8e1bef6bdbfb41e1
6
+ metadata.gz: 747f31b055917096f5e155bab0e9b4629a9a83ac1ca9978f70007831336ca7eac69848628a59fa9e957ec041864f9d052a2f9df618824b109b4f38f54f5eaa97
7
+ data.tar.gz: 34835c4db9ec8a8af5db3300cc8f21a18836b4b8251458ffa77b8f74bfcc82eadac5cd29b28131966721b788d87f0bacb6b0cb4f2c12cd992211cb6e9ad62682
data/README.md CHANGED
@@ -1,18 +1,20 @@
1
1
  # Blazer JSON API
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.
2
+ An extension to [Blazer](https://github.com/ankane/blazer) to enable exposing your queries as JSON via API, so that they can be consumed outside of Blazer by your application.
3
+ Use Blazer for powering in app charts using a charting library of your choice.
3
4
 
4
5
  ## Features
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.
6
+ - **Powered by SQL** Author APIs quickly using Blazers SQL based IDE. Particular useful for private/internal APIs that fall outside your standard API endpoints or where responses need to be taylored to suit a specific charting library.
7
+ - **No deploy APIs** Experimental APIs can be authored and iterated on quickly via Blazer without the need to do a deploy. Were a team is split between frontend and backend, this greatly increases collaboration and speed.
7
8
  - **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
+ - **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 granting everyone with access to Blazer also access to your APIs.
9
10
  - **URL parameters** URL parameters are also supported via Blazers query variables meaning the APIs can be highly dynamic and flexible
10
11
  - **Pagination** Pagination can be controlled using query variables in combination with limits and offsets
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
+ - **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 etc)
13
+ - **Permissions** Use Blazers [basic permissions mode](https://github.com/ankane/blazer#query-permissions) with your own naming conventions to control access and visibility of API based queries.
13
14
 
14
15
  ## Installation
15
16
  Follow the installation steps described to get [Blazer](https://github.com/ankane/blazer#installation) up and running.
17
+
16
18
  Then, add this line to your application's Gemfile:
17
19
 
18
20
  ```ruby
@@ -27,22 +29,40 @@ $ bundle
27
29
  And mount the engine in your `config/routes.rb`:
28
30
 
29
31
  ```ruby
30
- mount Blazer::Api::Engine, at: 'blazer-api'
32
+ mount BlazerJsonAPI::Engine, at: 'blazer-api'
31
33
  ```
32
34
 
33
- Configure authentication in an initializer as follows (e.g. in `initializers/blazer_api.rb`)
35
+ ## Authentication
36
+
37
+ Don’t forget to protect your Blazer APIs in production.
38
+
39
+ ### Basic authentication
40
+ Configure authentication in an initializer as follows (e.g. in `initializers/blazer_json_api.rb`)
34
41
 
35
42
  ```ruby
36
- Blazer::Api::Config.username = <api-username>
37
- Blazer::Api::Config.password = <api-password>
43
+ BlazerJsonAPI.configure do |config|
44
+ config.username = 'api-username'
45
+ config.password = 'api-password'
46
+ end
47
+ ```
48
+
49
+ ### Devise
50
+ Or alternatively, if you use devise, you can conditionally mount the engine using a policy or some user roles.
51
+
52
+ ```ruby
53
+ authenticate :user, ->(user) { user.admin? } do
54
+ mount BlazerJsonAPI::Engine, at: 'blazer-api'
55
+ end
38
56
  ```
39
57
 
40
58
  ## Usage
41
- Create queries as normal via Blazer and use the query identifier to render the JSON via the mounted location.
59
+ Create queries as normal via Blazer and use the query identifier to render the JSON via the mounted location.
42
60
 
43
61
  e.g. `/blazer-api/queries/1-all-users` or `/blazer-api/queries/1`
62
+
44
63
  URL params can be added where necessary also
45
- e.g. `/blazer-api/queries/1-all-users?page=1&per_page=30`
64
+
65
+ e.g. `/blazer-api/queries/1-all-users?username=blazer_user`
46
66
 
47
67
  ### Example queries
48
68
 
@@ -56,29 +76,29 @@ FROM users
56
76
  This would result in the following API response
57
77
  ```json
58
78
  [
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
- // ...
79
+ {
80
+ "id":1,
81
+ "username":"blazer_tommy",
82
+ "first_name":"Tom",
83
+ "last_name":"Carey",
84
+ "email":"tom.carey@gmail.com",
85
+ "country":"Ireland"
86
+ },
87
+ {
88
+ "id":2,
89
+ "username":"blazer_john",
90
+ "first_name":"John",
91
+ "last_name":"Doyle",
92
+ "email":"john.doyle@gmail.com",
93
+ "country":"USA"
94
+ }
76
95
  ]
77
96
  ```
78
97
  #### A simple single resource GET request using a variable
79
98
 
80
99
  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
100
+
101
+ **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
102
 
83
103
  ```sql
84
104
  SELECT id, username, first_name, last_name, email, country
@@ -87,19 +107,28 @@ WHERE username={username}
87
107
  LIMIT 1
88
108
  ```
89
109
  Now, the username can be passed as a URL parameter to the API to fetch the relevant record.
110
+
111
+ e.g. `/blazer-api/queries/1-all-users?username=blazer_john`
112
+
90
113
  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
- }
114
+
115
+ #### Using a variable/url parameter conditionally (e.g. optional filter)
116
+
117
+ It's possible to make a filter optional directly in SQL as follows
118
+
119
+ ```sql
120
+ SELECT id, username, first_name, last_name, email, country
121
+ FROM users
122
+ WHERE {username} IS NULL OR username={username}
100
123
  ```
124
+
125
+ In this scenario, the url parameter/filter for `username` is optional.
126
+
127
+ If provided it will apply the filter, if not provided, it will return an unfiltered response.
128
+
101
129
  #### Controlling response structure
102
130
  Standard queries return flat JSON responses that correspond to the results table from executing the SQL.
131
+
103
132
  It's possible to control the JSON structure by using double underscores to denote the desired nesting
104
133
 
105
134
  Take, for example, the following query:
@@ -142,6 +171,7 @@ Deeper nesting is also possible, just continue the pattern e.g. `a__deeper__nest
142
171
 
143
172
  #### Paginating potentially large responses
144
173
  If your query could return a large response, it's generally a good idea to paginate it.
174
+
145
175
  Pagination can be achieved in many ways, but a basic example can be done as follows using a combination
146
176
  of variables in the query and `LIMIT` and `OFFSET`.
147
177
 
@@ -152,9 +182,11 @@ LIMIT {per_page}
152
182
  OFFSET ({page}-1)*{per_page}
153
183
  ```
154
184
  Using this technique, URL params can be used by the requester to control pagination.
185
+
155
186
  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
187
 
157
188
  This technique can be used in combination with some default settings for these parameters in blazers config file `blazer.yml`.
189
+
158
190
  Having defaults means if they are not specified by the requester, the defaults will automatically be applied.
159
191
  ```yaml
160
192
  variable_defaults:
@@ -163,6 +195,17 @@ Having defaults means if they are not specified by the requester, the defaults w
163
195
  page: 1
164
196
  ```
165
197
 
198
+ Requests can then be as follows:
199
+
200
+ `/blazer-api/queries/1-all-users` = returns first 30 users (pagination defaults apply automatically)
201
+
202
+ `/blazer-api/queries/1-all-users?page=2` = returns second page containing 30 users
203
+
204
+ `/blazer-api/queries/1-all-users?page=1&per_page=90` = returns first page containing 90 users
205
+
206
+
207
+ etc...
208
+
166
209
  ## Contributing
167
210
  Want to improve this library, please do!
168
211
 
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 = 'Blazer::Api'
13
+ rdoc.title = 'BlazerJsonApi'
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/api .js
2
- //= link_directory ../stylesheets/blazer/api .css
1
+ //= link_directory ../javascripts/blazer_json_api .js
2
+ //= link_directory ../stylesheets/blazer_json_api .css
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ class ApplicationController < ActionController::Base
5
+
6
+ if BlazerJsonAPI.credentials_provided?
7
+ http_basic_authenticate_with name: BlazerJsonAPI.username, password: BlazerJsonAPI.password
8
+ end
9
+
10
+ protect_from_forgery with: :exception
11
+
12
+ def record_not_found
13
+ render json: [], status: :not_found
14
+ end
15
+
16
+ def render_errors(error_messages, status: :bad_request)
17
+ render json: { errors: error_messages }, status: status
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ class QueriesController < BlazerJsonAPI::ApplicationController
5
+ before_action :set_query
6
+
7
+ def show
8
+ @statement = @query.statement
9
+ data_source = @query.data_source
10
+ process_variables(@statement, data_source)
11
+ result = Blazer.data_sources[data_source].run_statement(@statement)
12
+
13
+ if result.error.present?
14
+ render_errors(Array(result.error))
15
+ else
16
+ render json: BlazerJsonAPI::ResultToNestedJson.new(@statement, result).call
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def set_query
23
+ @query = Blazer::Query.find_by(id: params[:id].to_s.split('-').first)
24
+ record_not_found && return if @query.blank?
25
+ end
26
+
27
+ def process_variables(statement, data_source)
28
+ BlazerJsonAPI::ProcessStatementVariables.new(statement, data_source, params).call
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ module ApplicationHelper
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,14 @@
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>
data/config/routes.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Blazer::Api::Engine.routes.draw do
3
+ BlazerJsonAPI::Engine.routes.draw do
4
4
  resources :queries, only: :show
5
5
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ module Configuration
5
+
6
+ VALID_OPTIONS_KEYS = %i[username password nesting_column_separator].freeze
7
+
8
+ DEFAULT_USERNAME = nil
9
+ DEFAULT_PASSWORD = nil
10
+ DEFAULT_NESTING_COLUMN_SEPARATOR = '__'.freeze
11
+
12
+ attr_accessor(*VALID_OPTIONS_KEYS)
13
+
14
+ def self.extended(base)
15
+ base.reset_config!
16
+ end
17
+
18
+ # Convenience method to allow configuration options to be set in a block
19
+ # e.g. in an initializer for the application
20
+ def configure
21
+ yield self
22
+ end
23
+
24
+ def credentials_provided?
25
+ username.present? && password.present?
26
+ end
27
+
28
+ def reset_config!
29
+ self.username = DEFAULT_USERNAME
30
+ self.password = DEFAULT_PASSWORD
31
+ self.nesting_column_separator = DEFAULT_NESTING_COLUMN_SEPARATOR
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace BlazerJsonAPI
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,53 @@
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
+ bind_variables.each do |variable|
18
+ value = params[variable].presence
19
+ if value
20
+ if variable.end_with?('_at')
21
+ begin
22
+ value = Blazer.time_zone.parse(value)
23
+ rescue StandardError
24
+ # do nothing
25
+ end
26
+ end
27
+
28
+ if /\A\d+\z/.match?(value.to_s)
29
+ value = value.to_i
30
+ elsif /\A\d+\.\d+\z/.match?(value.to_s)
31
+ value = value.to_f
32
+ end
33
+ end
34
+ if Blazer.transform_variable
35
+ value = Blazer.transform_variable.call(variable, value)
36
+ end
37
+ statement.gsub!("{#{variable}}", ActiveRecord::Base.connection.quote(value))
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def bind_variables
44
+ @bind_variables ||=
45
+ begin
46
+ (bind_variables ||= []).concat(Blazer.extract_vars(statement)).uniq!
47
+ bind_variables.each do |variable|
48
+ params[variable] ||= Blazer.data_sources[data_source].variable_defaults[variable]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,56 @@
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
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ VERSION = '0.1.4'
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'blazer_json_api/engine'
4
+ require 'blazer_json_api/configuration'
5
+ require 'blazer_json_api/process_statement_variables'
6
+ require 'blazer_json_api/result_to_nested_json'
7
+
8
+ module BlazerJsonAPI
9
+ extend BlazerJsonAPI::Configuration
10
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
4
+ require 'spec_helper'
5
+ ENV['RAILS_ENV'] ||= 'test'
6
+ require File.expand_path('../config/environment', __dir__)
7
+ # Prevent database truncation if the environment is production
8
+ if Rails.env.production?
9
+ abort('The Rails environment is running in production mode!')
10
+ end
11
+ require 'rspec/rails'
12
+ # Add additional requires below this line. Rails is not loaded until this point!
13
+
14
+ # Requires supporting ruby files with custom matchers and macros, etc, in
15
+ # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
16
+ # run as spec files by default. This means that files in spec/support that end
17
+ # in _spec.rb will both be required and run as specs, causing the specs to be
18
+ # run twice. It is recommended that you do not name files matching this glob to
19
+ # end with _spec.rb. You can configure this pattern with the --pattern
20
+ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
21
+ #
22
+ # The following line is provided for convenience purposes. It has the downside
23
+ # of increasing the boot-up time by auto-requiring all files in the support
24
+ # directory. Alternatively, in the individual `*_spec.rb` files, manually
25
+ # require only the support files necessary.
26
+ #
27
+ # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each do |f|
28
+ # require f
29
+ # end
30
+
31
+ # Checks for pending migrations and applies them before tests are run.
32
+ # If you are not using ActiveRecord, you can remove these lines.
33
+ begin
34
+ ActiveRecord::Migration.maintain_test_schema!
35
+ rescue ActiveRecord::PendingMigrationError => e
36
+ puts e.to_s.strip
37
+ exit 1
38
+ end
39
+ RSpec.configure do |config|
40
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
41
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
42
+
43
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
44
+ # examples within a transaction, remove the following line or assign false
45
+ # instead of true.
46
+ config.use_transactional_fixtures = true
47
+
48
+ # You can uncomment this line to turn off ActiveRecord support entirely.
49
+ # config.use_active_record = false
50
+
51
+ # RSpec Rails can automatically mix in different behaviours to your tests
52
+ # based on their file location, for example enabling you to call `get` and
53
+ # `post` in specs under `spec/controllers`.
54
+ #
55
+ # You can disable this behaviour by removing the line below, and instead
56
+ # explicitly tag your specs with their type, e.g.:
57
+ #
58
+ # RSpec.describe UsersController, type: :controller do
59
+ # # ...
60
+ # end
61
+ #
62
+ # The different available types are documented in the features, such as in
63
+ # https://relishapp.com/rspec/rspec-rails/docs
64
+ config.infer_spec_type_from_file_location!
65
+
66
+ # Filter lines from Rails gems in backtraces.
67
+ config.filter_rails_from_backtrace!
68
+ # arbitrary gems may also be filtered via:
69
+ # config.filter_gems_from_backtrace("gem name")
70
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by the `rails generate rspec:install` command.
4
+ # Conventionally, all
5
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
6
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
7
+ # this file to always be loaded, without a need to explicitly require it in any
8
+ # files.
9
+ #
10
+ # Given that it is always loaded, you are encouraged to keep this file as
11
+ # light-weight as possible. Requiring heavyweight dependencies from this file
12
+ # will add to the boot time of your test suite on EVERY test run, even for an
13
+ # individual file that may not need all of that loaded. Instead, consider making
14
+ # a separate helper file that requires the additional dependencies and performs
15
+ # the additional setup, and require it from the spec files that actually need
16
+ # it.
17
+ #
18
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
+ RSpec.configure do |config|
20
+ # rspec-expectations config goes here. You can use an alternate
21
+ # assertion/expectation library such as wrong or the stdlib/minitest
22
+ # assertions if you prefer.
23
+ config.expect_with :rspec do |expectations|
24
+ # This option will default to `true` in RSpec 4. It makes the `description`
25
+ # and `failure_message` of custom matchers include text for helper methods
26
+ # defined using `chain`, e.g.:
27
+ # be_bigger_than(2).and_smaller_than(4).description
28
+ # # => "be bigger than 2 and smaller than 4"
29
+ # ...rather than:
30
+ # # => "be bigger than 2"
31
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
32
+ end
33
+
34
+ # rspec-mocks config goes here. You can use an alternate test double
35
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
36
+ config.mock_with :rspec do |mocks|
37
+ # Prevents you from mocking or stubbing a method that does not exist on
38
+ # a real object. This is generally recommended, and will default to
39
+ # `true` in RSpec 4.
40
+ mocks.verify_partial_doubles = true
41
+ end
42
+
43
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
44
+ # have no way to turn it off -- the option exists only for backwards
45
+ # compatibility in RSpec 3). It causes shared context metadata to be
46
+ # inherited by the metadata hash of host groups and examples, rather than
47
+ # triggering implicit auto-inclusion in groups with matching metadata.
48
+ config.shared_context_metadata_behavior = :apply_to_host_groups
49
+
50
+ # The settings below are suggested to provide a good initial experience
51
+ # with RSpec, but feel free to customize to your heart's content.
52
+ # # This allows you to limit a spec run to individual examples or groups
53
+ # # you care about by tagging them with `:focus` metadata. When nothing
54
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
55
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
56
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
57
+ # config.filter_run_when_matching :focus
58
+ #
59
+ # # Allows RSpec to persist some state between runs in order to support
60
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
61
+ # # you configure your source control system to ignore this file.
62
+ # config.example_status_persistence_file_path = "spec/examples.txt"
63
+ #
64
+ # # Limits the available syntax to the non-monkey patched syntax that is
65
+ # # recommended. For more details, see:
66
+ # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
67
+ # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68
+ # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
69
+ # config.disable_monkey_patching!
70
+ #
71
+ # # Many RSpec users commonly either run the entire suite or an individual
72
+ # # file, and it's useful to allow more verbose output when running an
73
+ # # individual spec file.
74
+ # if config.files_to_run.one?
75
+ # # Use the documentation formatter for detailed output,
76
+ # # unless a formatter has already been configured
77
+ # # (e.g. via a command-line flag).
78
+ # config.default_formatter = "doc"
79
+ # end
80
+ #
81
+ # # Print the 10 slowest examples and example groups at the
82
+ # # end of the spec run, to help surface which specs are running
83
+ # # particularly slow.
84
+ # config.profile_examples = 10
85
+ #
86
+ # # Run specs in random order to surface order dependencies. If you find an
87
+ # # order dependency and want to debug it, you can fix the order by
88
+ # # providing
89
+ # # the seed, which is printed after each run.
90
+ # # --seed 1234
91
+ # config.order = :random
92
+ #
93
+ # # Seed global randomization in this process using the `--seed` CLI option.
94
+ # # Setting this allows you to use `--seed` to deterministically
95
+ # # reproduce
96
+ # # test failures related to randomization by passing the same `--seed`
97
+ # # value
98
+ # # as the one that triggered the failure.
99
+ # Kernel.srand config.seed
100
+ 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.1i
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Farrell
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-03 00:00:00.000000000 Z
11
+ date: 2021-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: blazer
14
+ name: railties
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '5'
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: '2.0'
26
+ version: '5'
27
27
  - !ruby/object:Gem::Dependency
28
- name: railties
28
+ name: blazer
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5'
33
+ version: '2.0'
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: '5'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec-rails
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -55,7 +55,7 @@ dependencies:
55
55
  description: An extension to the Blazer gem that makes it possible to expose queries
56
56
  as APIs
57
57
  email:
58
- - john.m.farrell1@gmail.com
58
+ - john.farrell@andopen.co
59
59
  executables: []
60
60
  extensions: []
61
61
  extra_rdoc_files: []
@@ -64,28 +64,30 @@ files:
64
64
  - README.md
65
65
  - Rakefile
66
66
  - app/assets/config/blazer_json_api_manifest.js
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
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
76
76
  - config/routes.rb
77
- - lib/blazer/api.rb
78
- - lib/blazer/api/config.rb
79
- - lib/blazer/api/engine.rb
80
- - lib/blazer/api/process_statement_variables.rb
81
- - lib/blazer/api/result_to_nested_json.rb
82
- - lib/blazer/api/version.rb
77
+ - lib/blazer_json_api.rb
78
+ - lib/blazer_json_api/configuration.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
83
83
  - lib/tasks/blazer_json_api_tasks.rake
84
- homepage: https://github.com/johnmfarrell1/blazer_json_api
84
+ - spec/rails_helper.rb
85
+ - spec/spec_helper.rb
86
+ homepage: https://gitlab.com/andopen/blazer_json_api
85
87
  licenses:
86
88
  - MIT
87
89
  metadata: {}
88
- post_install_message:
90
+ post_install_message:
89
91
  rdoc_options: []
90
92
  require_paths:
91
93
  - lib
@@ -96,12 +98,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
98
  version: '2.5'
97
99
  required_rubygems_version: !ruby/object:Gem::Requirement
98
100
  requirements:
99
- - - ">"
101
+ - - ">="
100
102
  - !ruby/object:Gem::Version
101
- version: 1.3.1
103
+ version: '0'
102
104
  requirements: []
103
- rubygems_version: 3.0.3
104
- signing_key:
105
+ rubygems_version: 3.1.2
106
+ signing_key:
105
107
  specification_version: 4
106
108
  summary: An API extension to the Blazer gem
107
- test_files: []
109
+ test_files:
110
+ - spec/spec_helper.rb
111
+ - spec/rails_helper.rb
@@ -1,17 +0,0 @@
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
@@ -1,36 +0,0 @@
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
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Blazer
4
- module Api
5
- module ApplicationHelper
6
- end
7
- end
8
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Blazer
4
- module Api
5
- class ApplicationJob < ActiveJob::Base
6
- end
7
- end
8
- end
@@ -1,10 +0,0 @@
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
@@ -1,9 +0,0 @@
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
@@ -1,14 +0,0 @@
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>
@@ -1,14 +0,0 @@
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
@@ -1,9 +0,0 @@
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
@@ -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 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
@@ -1,58 +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 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
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Blazer
4
- module Api
5
- VERSION = '0.1.1i'
6
- end
7
- end
data/lib/blazer/api.rb DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'blazer/api'
4
- require 'blazer/api/engine'
5
- require 'blazer/api/config'
6
- require 'blazer/api/process_statement_variables'
7
- require 'blazer/api/result_to_nested_json'
8
-
9
- module Blazer
10
- module Api
11
- end
12
- end