crazy_train 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9196c897891374e79b470c92f2e1d720eeebccc1e7997d05abe0a17ab0dbed59
4
+ data.tar.gz: 46a49259c0d4b009bfcc86dce18829e92b1f9d8604b346db614e29a50e897a9b
5
+ SHA512:
6
+ metadata.gz: 91831f51fc7e5b99e7ab4c498111d5c1a9718c34fe8b0a43a46ead63e2d24bcbaa476ed306643d1c2d4173479e7175b3ce1b9918779abded83c5101d42af8afc
7
+ data.tar.gz: 9b304548763006edf74d56ff0351926c6d24a492b69cc9340632d512fd887193365a3e61242c393b856224e9cff84103405dac27a973aa039fe6bffcda20a06a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright komagata
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,205 @@
1
+ ![CrazyTrain Logo](./crazy_train.png)
2
+
3
+ *I'm going off the rails on a crazy train.*
4
+
5
+ # CrazyTrain
6
+
7
+ *This gem is still unstable.*
8
+
9
+ Provides a RESTful API for database tables into your rails apps.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem "crazy_train"
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```console
22
+ $ bundle install
23
+ ```
24
+
25
+ Execute the below to generate a configuration file.
26
+
27
+ ```console
28
+ $ rails generate crazy_train:install
29
+ ```
30
+
31
+ ```ruby
32
+ # config/initializers/crazy_train.rb:
33
+ CrazyTrain.setup do |config|
34
+ config.secret = 'xxxxxxxxxxxxxxxx'
35
+ config.unauthorized_role = 'anonymous'
36
+ config.authorized_role = 'authenticated'
37
+ end
38
+ ```
39
+
40
+ Then add a line to routes.rb to mount the API.
41
+
42
+ ```ruby
43
+ # routes.rb:
44
+ Rails.application.routes.draw do
45
+ mount CrazyTrain::Engine, at: '/api'
46
+ end
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ crazy_train uses RLS for access control.
52
+
53
+ If accessed without authentication, it is accessed with `anonymous` ROLE.
54
+ If accessed with authentication, it is accessed with `authenticated` ROLE.
55
+
56
+ Set the authorization and RLS POLICY so that posts can `SELECT` even when unauthorized.
57
+
58
+ (Assume that you have a `posts` table in an existing rails project.)
59
+
60
+ ```sql
61
+ GRANT SELECT ON posts TO anonymous;
62
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
63
+ CREATE POLICY posts_policy ON posts TO anonymous USING (TRUE);
64
+ ```
65
+
66
+ Then you can use the REST API as follows.
67
+
68
+ ```console
69
+ $ curl http://localhost:3000/api/posts
70
+ [
71
+ {
72
+ "id": 1,
73
+ "content": "crazy"
74
+ },
75
+ {
76
+ "id": 2,
77
+ "content": "train"
78
+ }
79
+ ]
80
+ ```
81
+
82
+ You can use the API in the same way if you also set up permissions and POLICY settings for other tables and other ROLEs.
83
+
84
+ Access to the DB in rails is done by `default` ROLE, so it is not affected.
85
+
86
+ For complex items, create a Web API manually as usual, and use this for items that require a simple CRUD.
87
+
88
+ ## Authentication
89
+
90
+ crazy_train provides a mechanism for authentication in JWT.
91
+
92
+ You can generate a token using the `crazy_train:generate_token` task.
93
+ (secret uses the value of `CrazyTrain.config.secret`)
94
+
95
+ ```console
96
+ $ rails crazy_train:generate_token
97
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
98
+ ```
99
+
100
+ The token allows you to access the API in an authenticated state.
101
+
102
+ ```console
103
+ $ curl curl http://localhost:3000/api/posts/1234 \
104
+ -H "Authorization: Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
105
+ {
106
+ "id": 1234,
107
+ "content": "crazy"
108
+ }
109
+ ```
110
+
111
+ ### JWT-Based User Impersonation
112
+
113
+ What if I want to create another ROLE (e.g. `admin` ROLE) for authentication?
114
+
115
+ In crazy_train, if there is a `role` item in the JWT payload, it will be used as the ROLE name.
116
+
117
+ To create a token to authenticate with admin ROLE, do the following
118
+
119
+ ```console
120
+ $ rails crazy_train:generate_token PAYLOAD='{"role": "admin"}'
121
+ bbbbbbbbbbbbbbbbbbbbbbbbbbb
122
+ ```
123
+
124
+ When the API is accessed using this token, the DB is accessed as an `admin` ROLE.
125
+
126
+ ```console
127
+ $ curl curl http://localhost:3000/api/posts/1234 \
128
+ -H "Authorization: Bearer bbbbbbbbbbbbbbbbbbbbbbbbbbb
129
+ {
130
+ "id": 1234,
131
+ "content": "crazy"
132
+ }
133
+ ```
134
+
135
+ ### Individual user identification
136
+
137
+ What if I need to identify individual users to create a POLICY, such as "I can only view the `posts` I have posted"?
138
+
139
+ In crazy_train, the JWT payload can be retrieved via a user-defined configuration parameter in postgresql.
140
+
141
+ If you want to use the following as a PAYLOAD,
142
+
143
+ ```json
144
+ { "user_id": 1234 }
145
+ ```
146
+
147
+ In the DB, you can use `current_setting` to get it from `request.jwt.claims`.
148
+
149
+ ```sql
150
+ SELECT current_setting('request.jwt.claims', true)::json->>'user_id';
151
+ # => 1234
152
+ ```
153
+
154
+ The POLICY "Only your `posts`` can be viewed" can be written as follows.
155
+
156
+ ```sql
157
+ CREATE POLICY posts_policy ON posts
158
+ USING (user_id = current_setting('request.jwt.claims', true)::json->>'user_id');
159
+ ```
160
+
161
+ ## API
162
+
163
+ Allows CRUD to be performed on tables that exist in the DB.
164
+
165
+ | HTTP Verb | URL | Description |
166
+ | --------- | --- | ----------- |
167
+ | GET | `/api/posts` | List |
168
+ | GET | `/api/posts/1234` | Read |
169
+ | POST | `/api/posts` | Create |
170
+ | PUT | `/api/posts/1234` | Update |
171
+ | Delete | `/api/posts/1234` | Delete |
172
+
173
+ ```sh
174
+ $ curl localhost:3000/api/posts
175
+ [{"id":1,"body":"text 1"},{"id":2,"body":"text 2"},{"id":3,"body":"text 3"}]
176
+ ```
177
+
178
+ ```sh
179
+ $ curl localhost:3000/api/posts/1
180
+ {"id":1,"body":"text 1"}
181
+ ```
182
+
183
+ ### Ordering
184
+
185
+ Sorting can be performed using the `order` parameter. Column names and sort order are separated by `. `, and multiple rules are separated by `,`.
186
+
187
+ ```console
188
+ $ curl http://localhost:3000/api/posts?order=content.asc,id.desc
189
+ [
190
+ {
191
+ "id": 2,
192
+ "content": "train"
193
+ },
194
+ {
195
+ "id": 1,
196
+ "content": "crazy"
197
+ }
198
+ ]
199
+ ```
200
+
201
+ ## Contributing
202
+ Contribution directions go here.
203
+
204
+ ## License
205
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/setup'
2
+
3
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
4
+ load 'rails/tasks/engine.rake'
5
+
6
+ load 'rails/tasks/statistics.rake'
7
+
8
+ require 'bundler/gem_tasks'
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/crazy_train .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,47 @@
1
+ require 'jwt'
2
+
3
+ module CrazyTrain
4
+ class ApplicationController < ActionController::Base
5
+ handle_api_errors
6
+
7
+ before_action :verify_token
8
+ before_action :setup_role
9
+ after_action :teardown_role
10
+
11
+ def verify_token
12
+ @default_role = CrazyTrain.current_role
13
+ @role = if jwt_token && jwt_payload
14
+ jwt_payload['role'] || CrazyTrain.config.authenticated_role
15
+ else
16
+ CrazyTrain.config.unauthorized_role
17
+ end
18
+
19
+ payload_string = JSON.generate(jwt_payload)
20
+ CrazyTrain.setup_jwt_claims!(payload_string)
21
+ end
22
+
23
+ def setup_role
24
+ switch_role(@role)
25
+ end
26
+
27
+ def teardown_role
28
+ switch_role(@default_role)
29
+ end
30
+
31
+ def switch_role(role)
32
+ ActiveRecord::Base.connection.execute("SET ROLE #{role};")
33
+ end
34
+
35
+ def jwt_token
36
+ request.headers['Authorization'].split.last
37
+ rescue StandardError
38
+ nil
39
+ end
40
+
41
+ def jwt_payload
42
+ CrazyTrain::JWT.decode(jwt_token, CrazyTrain.config.secret).first
43
+ rescue StandardError
44
+ nil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ module CrazyTrain
2
+ class ResourcesController < ApplicationController
3
+ def index
4
+ query_builder = CrazyTrain::QueryBuilder.new(klass, params)
5
+ query_builder.parse!
6
+
7
+ records = query_builder.query
8
+ render json: records
9
+ end
10
+
11
+ def show
12
+ resource = klass.find(params[:id])
13
+ render json: resource
14
+ end
15
+
16
+ def create
17
+ klass.create!(resource_params.to_h)
18
+
19
+ head :created
20
+ end
21
+
22
+ def update
23
+ resource = klass.find(params[:id])
24
+ resource.update!(resource_params.to_h)
25
+
26
+ head :no_content
27
+ end
28
+
29
+ def destroy
30
+ resource = klass.find(params[:id])
31
+ resource.destroy!
32
+
33
+ head :no_content
34
+ end
35
+
36
+ private
37
+
38
+ def klass
39
+ name = params[:resource].singularize
40
+ CrazyTrain::Table.klass(name)
41
+ end
42
+
43
+ def resource_params
44
+ params.permit(*klass.column_names)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,7 @@
1
+ module CrazyTrain
2
+ class TablesController < ApplicationController
3
+ def index
4
+ render json: CrazyTrain::Table.names
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module CrazyTrain
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module CrazyTrain
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module CrazyTrain
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module CrazyTrain
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ module CrazyTrain
2
+ class OrderParser
3
+ attr_reader :orders
4
+
5
+ def initialize(order_text)
6
+ @order_text = order_text
7
+ @orders = {}
8
+ end
9
+
10
+ def parse!
11
+ @order_text.split(',').map do |text|
12
+ name, direction = text.split('.')
13
+ direction ||= 'asc'
14
+ @orders[name] = direction
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ module CrazyTrain
2
+ class QueryBuilder
3
+ attr_reader :klass, :params, :query, :orders
4
+
5
+ def initialize(klass, params)
6
+ @klass = klass
7
+ @params = params
8
+ @query = nil
9
+ @orders = []
10
+ end
11
+
12
+ def parse!
13
+ @query = klass.all
14
+
15
+ return unless params[:order]
16
+
17
+ @order_parser = CrazyTrain::OrderParser.new(params[:order])
18
+ @order_parser.parse!
19
+ @orders = @order_parser.orders
20
+ @query = @query.order(@orders)
21
+
22
+ # if params[:select]
23
+ # end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ module CrazyTrain
2
+ class Table
3
+ SYSTEM_TABLES = %w[ar_internal_metadata schema_migrations].freeze
4
+
5
+ class << self
6
+ def names
7
+ ActiveRecord::Base.connection.tables - SYSTEM_TABLES
8
+ end
9
+
10
+ def classes
11
+ names.map { |name| name.classify.constantize }
12
+ end
13
+
14
+ def klass(name)
15
+ name.classify.constantize
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,2 @@
1
+ <h1>Tables#index</h1>
2
+ <p>Find me in app/views/crazy_train/tables/index.html.erb</p>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Crazy train</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "crazy_train/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ CrazyTrain::Engine.routes.draw do
2
+ get 'tables' => 'tables#index', as: :tables, format: :json
3
+ get ':resource' => 'resources#index', as: :resources, format: :json
4
+ get ':resource/:id' => 'resources#show', as: :resource, format: :json
5
+ post ':resource' => 'resources#create', format: :json
6
+ patch ':resource/:id' => 'resources#update', format: :json
7
+ delete ':resource/:id' => 'resources#destroy', format: :json
8
+ end
@@ -0,0 +1,7 @@
1
+ require 'api_error_handler'
2
+
3
+ module CrazyTrain
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace CrazyTrain
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module CrazyTrain
2
+ class Grant
3
+ class << self
4
+ def execute!
5
+ sql "GRANT SELECT ON announcements TO #{CrazyTrain.config.unauthorized_role}"
6
+ sql "GRANT SELECT ON announcements TO #{CrazyTrain.config.authenticated_role}"
7
+ sql 'GRANT ALL PRIVILEGES ON announcements TO admin'
8
+ sql 'GRANT ALL PRIVILEGES ON users TO admin'
9
+ sql 'GRANT ALL PRIVILEGES ON notes TO admin'
10
+ sql 'GRANT USAGE ON announcements_id_seq TO admin'
11
+ end
12
+
13
+ def sql(statement)
14
+ ActiveRecord::Base.connection.execute(statement)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ module CrazyTrain
2
+ class JWT
3
+ HEADERS = { 'alg' => 'HS256', 'typ' => 'JWT' }.freeze
4
+
5
+ def self.encode(payload, secret = CrazyTrain.config.secret)
6
+ ::JWT.encode(payload, secret, 'HS256', HEADERS)
7
+ end
8
+
9
+ def self.decode(token, secret = CrazyTrain.config.secret)
10
+ ::JWT.decode(token, secret, true, HEADERS)
11
+ end
12
+
13
+ def self.generate_token(payload_string)
14
+ payload = JSON.parse(payload_string || '{}')
15
+ encode(payload)
16
+ end
17
+
18
+ def self.generate_jwt_secret
19
+ SecureRandom.alphanumeric(32)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module CrazyTrain
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module CrazyTrain
2
+ VERSION = '0.2.0'.freeze
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'crazy_train/version'
2
+ require 'crazy_train/engine'
3
+ require 'crazy_train/jwt'
4
+ require 'jwt'
5
+
6
+ module CrazyTrain
7
+ Config = Struct.new(:secret, :unauthorized_role, :authenticated_role)
8
+ @@config = Config.new(
9
+ secret: 'you-must-change-this',
10
+ unauthorized_role: 'anonymous',
11
+ authenticated_role: 'authenticated'
12
+ )
13
+
14
+ def self.config
15
+ @@config
16
+ end
17
+
18
+ def self.setup
19
+ yield @@config
20
+ end
21
+
22
+ def self.current_role
23
+ ActiveRecord::Base.connection.execute('SELECT current_user').to_a.first['current_user']
24
+ end
25
+
26
+ def self.current_settings
27
+ ActiveRecord::Base.connection.execute('SELECT current_setting').to_a.first['current_setting']
28
+ end
29
+
30
+ def self.setup_jwt_claims!(paylaod)
31
+ ActiveRecord::Base.connection.execute("SET request.jwt.claims = '#{paylaod}'")
32
+ end
33
+
34
+ def self.request_jwt_claims
35
+ sql = "SELECT current_setting('request.jwt.claims', true) AS request_jwt_claims"
36
+ string = ActiveRecord::Base.connection.execute(sql).to_a.first['request_jwt_claims']
37
+ JSON.parse(string)
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ require 'rails/generators'
2
+
3
+ module CrazyTrain
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ def copy_initializer
9
+ template 'initializer_template.erb', 'config/initializers/crazy_train.rb'
10
+ end
11
+
12
+ def add_route
13
+ route "mount CrazyTrain::Engine, at: '/api'"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ CrazyTrain.setup do |config|
2
+ config.secret = '<%= CrazyTrain::JWT.generate_jwt_secret %>'
3
+ config.unauthorized_role = 'anonymous'
4
+ config.authorized_role = 'authenticated'
5
+ end
@@ -0,0 +1,7 @@
1
+ require 'crazy_train'
2
+
3
+ namespace :crazy_train do
4
+ task generate_token: :environment do
5
+ puts CrazyTrain::JWT.generate_token(ENV.fetch('PAYLOAD', nil))
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crazy_train
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - komagata
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-12-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: api_error_handler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 2.2.1
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '2.2'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.2.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 6.0.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 6.0.0
61
+ description: Provides a RESTful API for database tables for your rails apps.
62
+ email:
63
+ - komagata@gmail.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - MIT-LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - app/assets/config/crazy_train_manifest.js
72
+ - app/assets/stylesheets/crazy_train/application.css
73
+ - app/controllers/crazy_train/application_controller.rb
74
+ - app/controllers/crazy_train/resources_controller.rb
75
+ - app/controllers/crazy_train/tables_controller.rb
76
+ - app/helpers/crazy_train/application_helper.rb
77
+ - app/jobs/crazy_train/application_job.rb
78
+ - app/mailers/crazy_train/application_mailer.rb
79
+ - app/models/crazy_train/application_record.rb
80
+ - app/models/crazy_train/order_parser.rb
81
+ - app/models/crazy_train/query_builder.rb
82
+ - app/models/crazy_train/table.rb
83
+ - app/views/crazy_train/tables/index.html.erb
84
+ - app/views/layouts/crazy_train/application.html.erb
85
+ - config/routes.rb
86
+ - lib/crazy_train.rb
87
+ - lib/crazy_train/engine.rb
88
+ - lib/crazy_train/grant.rb
89
+ - lib/crazy_train/jwt.rb
90
+ - lib/crazy_train/railtie.rb
91
+ - lib/crazy_train/version.rb
92
+ - lib/generators/crazy_train/install/install_generator.rb
93
+ - lib/generators/crazy_train/install/templates/initializer_template.erb
94
+ - lib/tasks/crazy_train.rake
95
+ homepage: https://github.com/komagata/crazy_train
96
+ licenses: []
97
+ metadata:
98
+ homepage_uri: https://github.com/komagata/crazy_train
99
+ source_code_uri: https://github.com/komagata/crazy_train
100
+ changelog_uri: https://github.com/komagata/crazy_train/CHANGELOG.md
101
+ rubygems_mfa_required: 'true'
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 2.6.0
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.4.22
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Provides a RESTful API to DB tables.
121
+ test_files: []