blazer_json_api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f9997698b1f7539ecc259833efc06cb6b673c3afe970113ce6f898218392bd4f
4
+ data.tar.gz: 025bf36ee3a803271b062446c4b7b1bbba2eae9b9ed8f5c59e0e87a92d10c0e9
5
+ SHA512:
6
+ metadata.gz: e557025ae1c358e7bcc0d3aac99bed5968b0ab4af95bd3e10fb55ca7f959fc06c10f2d169916635db75125c35e857a9c940d613e5557d2fd85f25536f199e569
7
+ data.tar.gz: 9dc9425a8908209ae17b4148ac82115db3122c283bacab2d1b64819a2cefff0b183da4637cb9ec65ecf6f534a42a3cd2ec1e22e3cb8efe6b690df02be546cd25
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 john.farrell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
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.
3
+
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
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)
12
+
13
+ ## Installation
14
+ Follow the installation steps described to get Blazer up and running.
15
+ Then, add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'blazer_json_api'
19
+ ```
20
+
21
+ And then execute:
22
+ ```bash
23
+ $ bundle
24
+ ```
25
+
26
+ And mount the engine in your `config/routes.rb`:
27
+
28
+ ```ruby
29
+ mount BlazerJsonAPI, at: 'blazer-api'
30
+ ```
31
+
32
+ Configure authentication in an initializer as follows (e.g. in `initializers/blazer_json_api.rb`)
33
+
34
+ ```ruby
35
+ BlazerJsonAPI::Config.username = <api-username>
36
+ BlazerJsonAPI::Config.password = <api-password>
37
+ ```
38
+
39
+ ## Usage
40
+ Create queries as normal via Blazer and use the query identifier to render the JSON via the mounted location.
41
+
42
+ e.g. `/blazer-api/queries/1-all-users`
43
+
44
+ ### Example queries
45
+
46
+
47
+ > TODO
48
+
49
+ ## Contributing
50
+
51
+
52
+ > TODO
53
+
54
+ ## License
55
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'BlazerJsonApi'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
20
+ load 'rails/tasks/engine.rake'
21
+
22
+ load 'rails/tasks/statistics.rake'
23
+
24
+ require 'bundler/gem_tasks'
25
+
26
+ require 'rake/testtask'
27
+
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'lib'
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = false
33
+ end
34
+
35
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/blazer_json_api .js
2
+ //= link_directory ../stylesheets/blazer_json_api .css
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,34 @@
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
@@ -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 ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ BlazerJsonAPI::Engine.routes.draw do
4
+ resources :queries, only: :show
5
+ end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace BlazerJsonAPI
6
+ end
7
+ 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 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
@@ -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::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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlazerJsonAPI
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :blazer_json_api do
4
+ # # Task goes here
5
+ # end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blazer_json_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - John Farrell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-11-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: blazer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: An extension to the Blazer gem that makes it possible to expose queries
56
+ as APIs
57
+ email:
58
+ - john.m.farrell1@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - Rakefile
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
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
83
+ - lib/tasks/blazer_json_api_tasks.rake
84
+ homepage: https://github.com/johnmfarrell1/blazer_json_api
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.0.3
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: An API extension to the Blazer gem
107
+ test_files: []