rrx_api 0.1.0 → 8.0.3

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/Gemfile +3 -4
  4. data/Gemfile.lock +127 -118
  5. data/README.md +1 -1
  6. data/app/controllers/concerns/rrx_api/authenticatable.rb +79 -0
  7. data/app/controllers/rrx_api/controller.rb +1 -0
  8. data/app/controllers/rrx_api/health_controller.rb +33 -0
  9. data/app/models/concerns/arel_query.rb +189 -0
  10. data/app/models/rrx_api/record.rb +12 -0
  11. data/config/routes.rb +13 -0
  12. data/lib/generators/rrx_api/base.rb +95 -0
  13. data/lib/generators/rrx_api/docker_generator.rb +19 -0
  14. data/lib/generators/rrx_api/github_generator.rb +83 -0
  15. data/lib/generators/rrx_api/install_generator.rb +120 -0
  16. data/lib/generators/rrx_api/templates/docker/Dockerfile.tt +1 -0
  17. data/lib/generators/rrx_api/templates/github/build/workflows/build.yml.tt +71 -0
  18. data/lib/generators/rrx_api/templates/github/deploy/workflows/deploy.yml.tt +62 -0
  19. data/lib/generators/rrx_api/templates/terraform/aws/iam.tf.tt +37 -0
  20. data/lib/generators/rrx_api/templates/terraform/aws/main.tf.tt +44 -0
  21. data/lib/generators/rrx_api/templates/terraform/aws/service.tf.tt +67 -0
  22. data/lib/generators/rrx_api/terraform_generator.rb +76 -0
  23. data/lib/rrx_api/auth/base.rb +29 -0
  24. data/lib/rrx_api/auth/firebase.rb +70 -0
  25. data/lib/rrx_api/engine.rb +111 -0
  26. data/lib/rrx_api/version.rb +3 -2
  27. data/lib/rrx_api.rb +1 -1
  28. metadata +69 -37
  29. data/.idea/.gitignore +0 -8
  30. data/.idea/inspectionProfiles/Project_Default.xml +0 -6
  31. data/.idea/modules.xml +0 -8
  32. data/.idea/rrx_api.iml +0 -255
  33. data/.idea/vcs.xml +0 -6
  34. data/exe/rrx_api_setup +0 -37
  35. data/exe/sources/config/initializers/cors.rb +0 -21
  36. data/exe/sources/config/initializers/generators.rb +0 -6
  37. data/lib/rrx_api/railtie.rb +0 -44
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArelQuery
4
+ extend ActiveSupport::Concern
5
+
6
+ module Cast
7
+ def self.included(mod)
8
+ mod.class_eval do
9
+ alias_method :create_cast, :cast if method_defined?(:cast)
10
+
11
+ def cast(type)
12
+ ::Arel::Nodes::NamedFunction.new "CAST", [self.as(type)]
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ class WithHelper
19
+ attr_reader :name
20
+
21
+ def initialize(name, relation)
22
+ raise ArgumentError, 'Name must be a Symbol or String' unless name.is_a?(Symbol) || name.is_a?(String)
23
+ raise ArgumentError, 'Relation must be an Arel node or select manager' unless relation.is_a?(Arel::Nodes::Node) || relation.is_a?(Arel::SelectManager)
24
+
25
+ @name = name
26
+ @relation = relation
27
+ end
28
+
29
+ def [](column)
30
+ table[column]
31
+ end
32
+
33
+ def table
34
+ @table ||= Arel::Table.new(@name)
35
+ end
36
+
37
+ def to_cte
38
+ Arel::Nodes::Cte.new(@name, @relation)
39
+ end
40
+ end
41
+
42
+ class ArelHelper
43
+ attr_reader :model
44
+
45
+ delegate :star, :sql, to: Arel
46
+ delegate :project, :where, :from, :join, :outer_join, :group, :order, :alias, to: :@table
47
+ delegate :with, to: :from
48
+
49
+ def initialize(model)
50
+ @model = model
51
+ @table = model.arel_table if model.respond_to?(:arel_table)
52
+ end
53
+
54
+ def table(name = nil)
55
+ if name
56
+ Arel::Table.new(name)
57
+ elsif @table
58
+ @table
59
+ else
60
+ raise ArgumentError, 'No table available. Provide a table name.'
61
+ end
62
+ end
63
+
64
+ alias t table
65
+
66
+ def literal(value)
67
+ Arel::Nodes::SqlLiteral.new(value.to_s)
68
+ end
69
+
70
+ alias l literal
71
+ alias lit literal
72
+
73
+ def string(value)
74
+ literal "'#{value}'"
75
+ end
76
+
77
+ alias s string
78
+ alias str string
79
+
80
+ # @return [Arel::SelectManager]
81
+ def select(...)
82
+ Arel::SelectManager.new(...)
83
+ end
84
+
85
+ def as(what, alias_name)
86
+ raise ArgumentError, 'Alias name must be a Symbol or String' unless alias_name.is_a?(Symbol) || alias_name.is_a?(String)
87
+
88
+ Arel::Nodes::As.new(what, literal(alias_name))
89
+ end
90
+
91
+ def for_with(name, relation)
92
+ WithHelper.new(name, relation)
93
+ end
94
+
95
+ def results(query)
96
+ connection.select_all(query)
97
+ end
98
+
99
+ def rows(query)
100
+ results(query).to_a
101
+ end
102
+
103
+ def connection
104
+ ActiveRecord::Base.connection
105
+ end
106
+
107
+ def respond_to_missing?(_method_name, _include_private = false)
108
+ true
109
+ end
110
+
111
+ FUNCTION_METHOD_PATTERN = /\A[a-z]+(_[a-z]+)*\z/
112
+
113
+ def method_missing(method_name, *args, &block)
114
+ node_class = "Arel::Nodes::#{method_name.to_s.camelize}".safe_constantize
115
+
116
+ if node_class
117
+ self.class.define_method method_name do |*method_args|
118
+ node_class.new(*method_args)
119
+ end
120
+ elsif @table&.respond_to?(method_name)
121
+ return @table.send(method_name, *args)
122
+ elsif method_name.to_s =~ FUNCTION_METHOD_PATTERN
123
+ self.class.define_method method_name do |*method_args|
124
+ Arel::Nodes::NamedFunction.new(method_name.to_s, method_args)
125
+ end
126
+ else
127
+ return super
128
+ end
129
+
130
+ send(method_name, *args)
131
+ end
132
+
133
+ def column(name)
134
+ @table[name]
135
+ end
136
+
137
+ alias col column
138
+ alias c column
139
+
140
+ def [](it)
141
+ case it
142
+ when Symbol
143
+ @table[it]
144
+ when String
145
+ string(it)
146
+ else
147
+ literal(it)
148
+ end
149
+ end
150
+ end
151
+
152
+ class_methods do
153
+ def aq(*args)
154
+ @arel_helper ||= ArelHelper.new(self)
155
+ args.empty? ? @arel_helper : @arel_helper.sql(*args)
156
+ end
157
+ end
158
+
159
+ included do
160
+ def aq(...)
161
+ self.class.aq(...)
162
+ end
163
+ end
164
+ end
165
+
166
+ ActiveSupport.on_load :active_record do
167
+ class ::Arel::SelectManager
168
+ def table
169
+ @ast.cores[0].source.left
170
+ end
171
+
172
+ def join_to(other, from: :id, to: :id, on: nil, type: :inner)
173
+ on ||= table[from].eq(other[to])
174
+ join_node_class = case type
175
+ when :inner then Arel::Nodes::InnerJoin
176
+ when :left, :outer then Arel::Nodes::OuterJoin
177
+ when :right then Arel::Nodes::RightOuterJoin
178
+ when :full then Arel::Nodes::FullOuterJoin
179
+ else raise ArgumentError, "Unknown join type: #{type.inspect}"
180
+ end
181
+
182
+ other = other.table if other.respond_to?(:table)
183
+ join(other, join_node_class).on(on)
184
+ end
185
+ end
186
+
187
+ ::Arel::Nodes::Node.include(ArelQuery::Cast)
188
+ ::Arel::Attributes::Attribute.include(ArelQuery::Cast)
189
+ end
@@ -4,9 +4,21 @@ module RrxApi
4
4
  class Record < ActiveRecord::Base
5
5
  self.abstract_class = true
6
6
 
7
+ include ArelQuery
8
+
9
+ before_create :set_new_id
10
+
7
11
  # @return [RrxLogging::Logger]
8
12
  def logger
9
13
  @logger ||= RrxLogging.current || Rails.logger
10
14
  end
15
+
16
+ protected
17
+
18
+ def set_new_id
19
+ self.id ||= Random.uuid if has_attribute?(:id)
20
+ end
21
+
11
22
  end
23
+
12
24
  end
data/config/routes.rb ADDED
@@ -0,0 +1,13 @@
1
+ RrxApi::Engine.routes.draw do
2
+ if Rails.root.join('swagger').exist?
3
+ require 'rswag/api/engine'
4
+ require 'rswag/ui/engine'
5
+
6
+ mount Rswag::Ui::Engine => '/api-docs'
7
+ mount Rswag::Api::Engine => '/api-docs'
8
+ end
9
+
10
+ healthcheck_path = Rails.application.config.healthcheck_route
11
+ healthcheck_path = "/#{healthcheck_path}" unless healthcheck_path.start_with?('/')
12
+ get healthcheck_path => 'rrx_api/health#show', as: :rrx_health_check
13
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RrxApi
4
+ module Generators
5
+ class Base < ::Rails::Generators::Base
6
+ hide!
7
+
8
+ def self.init!(description = nil)
9
+ # noinspection RubyMismatchedArgumentType
10
+ source_root Pathname(__dir__).join('templates')
11
+ desc description if description
12
+ end
13
+
14
+ protected
15
+
16
+ # class Version
17
+ # def initialize(str)
18
+ # @version = str.to_s
19
+ # @segments = @version.split('.').map(&:to_i)
20
+ # end
21
+ #
22
+ # def to_s
23
+ # @version
24
+ # end
25
+ #
26
+ # def major
27
+ # @segments[0]
28
+ # end
29
+ #
30
+ # def minor
31
+ # @segments[1]
32
+ # end
33
+ #
34
+ # def build
35
+ # @segments[2] || 0
36
+ # end
37
+ #
38
+ # def major_minor
39
+ # "#{major}.#{minor}"
40
+ # end
41
+ #
42
+ # def <=>(other)
43
+ # return nil unless other.is_a?(Version)
44
+ #
45
+ # result = major <=> other.major
46
+ # result = minor <=> other.minor if result.zero?
47
+ # result = build <=> other.build if result.zero?
48
+ # result
49
+ # end
50
+ # end
51
+
52
+ def destination_path
53
+ @destination_path = Pathname(destination_root)
54
+ end
55
+
56
+ def app_name
57
+ @app_name ||= destination_path.basename.to_s
58
+ end
59
+
60
+ # @return [String] Current Ruby version in format "MAJOR.MINOR"
61
+ def ruby_version
62
+ @ruby_version ||= bundle_max_ruby_version || current_ruby_version
63
+ end
64
+
65
+ # @return [Bundler::Definition]
66
+ def bundle
67
+ @bundle ||= Bundler::Definition.build(
68
+ destination_path.join('Gemfile'),
69
+ destination_path.join('Gemfile.lock'),
70
+ nil
71
+ )
72
+ end
73
+
74
+ def current_ruby_version
75
+ RUBY_VERSION.split('.')[0..1].join('.')
76
+ end
77
+
78
+ def bundle_max_ruby_version
79
+ return nil unless bundle.ruby_version
80
+
81
+ version = Gem::Requirement.new(*bundle.ruby_version.versions)
82
+ max_minor = version
83
+ .requirements
84
+ .map { |_, v| v.segments[0..1] } # [major, minor]
85
+
86
+ if max_minor.any?
87
+ max_minor.min_by { |major, minor| [major, minor] }.join('.')
88
+ else
89
+ nil
90
+ end
91
+
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module RrxApi
6
+ module Generators
7
+ class DockerGenerator < Base
8
+ init! 'Generates configuration files for building a docker image.'
9
+
10
+ class_option :image_name, type: :string, desc: 'Name of the Docker image. Defaults to application name.'
11
+
12
+ def docker
13
+ template 'docker/Dockerfile.tt', 'Dockerfile'
14
+ end
15
+
16
+ alias image_name app_name
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'base'
3
+
4
+ module RrxApi
5
+ module Generators
6
+ class GithubGenerator < Base
7
+ init! 'Generates GitHub Actions workflows for building and deploying the application using Docker.'
8
+
9
+ class_option :deploy,
10
+ type: :boolean,
11
+ default: false,
12
+ desc: 'Include deployment workflow'
13
+
14
+ class_option :terraform_repo,
15
+ type: :string,
16
+ default: 'terraform',
17
+ desc: 'Terraform repository name (if using deployment workflow)'
18
+
19
+ class_option :terraform_module,
20
+ type: :string,
21
+ desc: 'Terraform module name (if using deployment workflow). Default is the application name.'
22
+
23
+ class_option :database,
24
+ type: :string,
25
+ default: 'auto',
26
+ enum: %w[auto postgresql mysql mariadb sqlite none],
27
+ desc: 'Database type'
28
+
29
+ def github
30
+ directory 'github/build', '.github'
31
+ directory 'github/deploy', '.github' if deploy?
32
+ end
33
+
34
+ private
35
+
36
+ def deploy?
37
+ options[:deploy]
38
+ end
39
+
40
+ def database
41
+ @database ||= options[:database] == 'auto' ? detect_database : options[:database]
42
+ end
43
+
44
+ def terraform_repo
45
+ options[:terraform_repo]
46
+ end
47
+
48
+ def terraform_module
49
+ options[:terraform_module] || app_name
50
+ end
51
+
52
+ def docker_packages
53
+ ''
54
+ end
55
+
56
+ def detect_database
57
+ config_path = destination_path.join('config/database.yml')
58
+ if config_path.exist?
59
+ # @type {Hash}
60
+ config = YAML.safe_load(config_path.read, symbolize_names: true, aliases: true)
61
+ adapter = config.dig(:test, :adapter).to_s.downcase
62
+ case adapter
63
+ when /postgresql/, /psql/
64
+ 'postgresql'
65
+ when /mysql/
66
+ if yes?('Detected MySQL adapter in database.yml. Are you using MariaDB? (Yn)')
67
+ 'mariadb'
68
+ else
69
+ 'mysql'
70
+ end
71
+ when /sqlite/
72
+ 'sqlite'
73
+ else
74
+ say_error 'Unsupported database adapter detected in config/database.yml. Please specify the database type explicitly using --database option.'
75
+ exit 1
76
+ end
77
+ else
78
+ 'none'
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+ # frozen_string_literal: true
3
+ require_relative 'base'
4
+
5
+ module RrxApi
6
+ module Generators
7
+ class InstallGenerator < Base
8
+ init! 'Installs the RRX API gem and its dependencies.'
9
+
10
+ class_option :skip_rrx_dev, type: :boolean, default: false, hide: true
11
+
12
+ # Updates the application configuration file with specific dependencies and settings.
13
+ # The method performs the following operations:
14
+ # - Reads the application configuration file located at 'config/application.rb'.
15
+ # - Removes existing comments and unnecessary `require` statements from the file.
16
+ # - Includes necessary `require` directives for the application's gem dependencies.
17
+ # - Injects or updates the Bundler require statement to include the necessary gems.
18
+ # - Cleans up unwanted whitespace in the file content.
19
+ # - Rewrites the configuration file with the updated content.
20
+ # - Appends additional configuration settings for time zone, schema format, and session management.
21
+ #
22
+ # @return [void] Since the primary purpose is file modification, it does not return a value directly.
23
+ def update_application
24
+ # @type [Pathname]
25
+ app_file = Pathname(destination_root).join('config', 'application.rb')
26
+ app_code = app_file.read
27
+
28
+ # Assume full replace if we've never modified before.
29
+ # Otherwise, create_file will prompt to replace it.
30
+ remove_file app_file unless app_code =~ /rrx_api/
31
+
32
+ app_code.gsub!(/^\s*#.*\r?\n/, '')
33
+ app_code.gsub!(/^(?:#\s+)?require ["'].*\r?\n/, '')
34
+
35
+ requires = application_gems.map do |gem|
36
+ "require '#{gem}'"
37
+ end.join("\n")
38
+
39
+ app_code.sub!(/^(Bundler.require.*)$/) do |str|
40
+ <<~REQ
41
+ #{requires}
42
+
43
+ #{str}
44
+ REQ
45
+ end
46
+
47
+ # Remove existing application config lines
48
+ APPLICATION_CONFIG.each do |line|
49
+ app_code.gsub!(/^\s*#{line}\W*.*\n/, '')
50
+ end
51
+
52
+ # Remove unnecessary whitespace
53
+ app_code.lstrip!
54
+ app_code.gsub!(/^\s*\r?\n(\s*\r?\n)+/, "\n")
55
+
56
+ # puts app_code
57
+ create_file app_file, app_code
58
+ end
59
+
60
+ def update_base_classes
61
+ gsub_file 'app/models/application_record.rb',
62
+ /ApplicationRecord.*/,
63
+ 'ApplicationRecord < RrxApi::Record'
64
+
65
+ gsub_file 'app/controllers/application_controller.rb',
66
+ /ApplicationController.*/,
67
+ 'ApplicationController < RrxApi::Controller'
68
+ end
69
+
70
+ def routes
71
+ inject_into_file 'config/routes.rb',
72
+ after: "Rails.application.routes.draw do\n" do
73
+ <<~RUBY
74
+ mount RrxApi::Engine => '/'
75
+ RUBY
76
+ end
77
+ end
78
+
79
+ def rrx_dev
80
+ generate 'rrx_dev:install' unless options[:skip_rrx_dev]
81
+ end
82
+
83
+ def asdf_versions
84
+ create_file '.tool-versions', <<~VERSIONS
85
+ ruby #{ruby_version}
86
+ VERSIONS
87
+ end
88
+
89
+ private
90
+
91
+ # Configs to remove from the application.rb file
92
+ APPLICATION_CONFIG = <<~CONFIG.split("\n").map(&:strip).freeze
93
+ config.time_zone
94
+ config.active_support.to_time_preserves_timezone
95
+ config.active_record.schema_format
96
+ config.session_store
97
+ config.middleware.use ActionDispatch::Cookies
98
+ config.middleware.use ActionDispatch::Session::CookieStore
99
+ CONFIG
100
+
101
+ # @return [Array<String>] The list of application gem names that are dependencies
102
+ def application_gems
103
+ gems = %w[rrx_api]
104
+ gems.concat(%w[rrx_jobs active_job].select { |name| gem?(name) })
105
+ end
106
+
107
+ # @param [String] name
108
+ # @return [Boolean] True if gem is a dependency
109
+ def gem?(name)
110
+ bundle_gems.include?(name)
111
+ end
112
+
113
+ # @return [Set<String>]
114
+ def bundle_gems
115
+ @bundle_gems ||= bundle.dependencies.map(&:name).to_set
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1 @@
1
+ FROM ddrew555/rrx_docker:<%= ruby_version %>
@@ -0,0 +1,71 @@
1
+ name: Build
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ <% if deploy? %>
6
+ inputs:
7
+ dev_deploy:
8
+ type: boolean
9
+ default: true
10
+ description: 'Deploy build to development environment'
11
+ <% end %>
12
+
13
+ push:
14
+ branches: [main]
15
+
16
+ env:
17
+ image_name: '<%= app_name %>'
18
+ major_version: 1
19
+ minor_version: 0
20
+ development: ${{ github.ref_name != 'main' }}
21
+
22
+ permissions: write-all
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ with:
30
+ fetch-depth: 0
31
+
32
+ - uses: dan-drew/asdf-actions/tool-versions@v1
33
+
34
+ - name: Test
35
+ uses: rails-rrx/actions/test@main
36
+ with:
37
+ ruby_version: <%= ruby_version %>
38
+ database: <%= database %>
39
+
40
+ - uses: dan-drew/actions/next-version@main
41
+ with:
42
+ suffix: ${{ env.development && '-dev' || '' }}
43
+
44
+ - name: Build
45
+ uses: rails-rrx/actions/docker-build@main
46
+ with:
47
+ repository: ${{ vars.DOCKER_REPOSITORY }}
48
+ username: ${{ secrets.DOCKER_USERNAME }}
49
+ password: ${{ secrets.DOCKER_PASSWORD }}
50
+ image_name: $${{ env.image_name }}
51
+ image_version: ${{ env.next_version }}
52
+ database: <%= database %>
53
+ latest: true
54
+ packages: '<%= docker_packages %>'
55
+
56
+ - uses: dan-drew/actions/push-version@main
57
+ if: ${{ ! env.development }}
58
+ <% if deploy? %>
59
+ - name: Deploy Development
60
+ uses: actions/github-script@v7
61
+ condition: ${{ inputs.dev_deploy }}
62
+ with:
63
+ script: |
64
+ github.rest.actions.createWorkflowDispatch({
65
+ owner: context.repo.owner,
66
+ repo: context.repo.repo,
67
+ workflow_id: 'deploy.yml',
68
+ ref: context.ref,
69
+ inputs: { version: '${{ env.next_version }}' }
70
+ });
71
+ <% end %>
@@ -0,0 +1,62 @@
1
+ name: Deploy
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ required: true
8
+ type: string
9
+ description: 'Version to deploy'
10
+ environment:
11
+ default: development
12
+ type: string
13
+ description: 'Environment to deploy'
14
+
15
+ env:
16
+ image_name: '<%= app_name %>'
17
+ image_tag: "${{ inputs.version }}"
18
+ terraform_repo: '<%= terraform_repo %>'
19
+ terraform_module: '<%= terraform_module %>'
20
+ repository: ${{ vars.DOCKER_REPOSITORY }}
21
+ username: ${{ secrets.DOCKER_USERNAME }}
22
+ password: ${{ secrets.DOCKER_PASSWORD }}
23
+
24
+ permissions: write-all
25
+
26
+ jobs:
27
+ deploy:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - name: Tag Docker Image
31
+ run: |
32
+ readonly image_ref="${repository}/${image_name}"
33
+ readonly source_image="${image_ref}:${image_tag}"
34
+ readonly dest_image="${image_ref}:${{ inputs.environment }}"
35
+
36
+ echo ::group::Login
37
+ cat <<-PASSWORD | docker login "$repository" -u "$username" --password-stdin
38
+ ${password}
39
+ PASSWORD
40
+ echo ::endgroup::
41
+
42
+ echo ::group::Tag
43
+ docker pull -q "${source_image}"
44
+ docker tag "${source_image}" "${dest_image}"
45
+ docker push -q "${dest_image}"
46
+ echo ::endgroup::
47
+
48
+ - name: Trigger Terraform
49
+ uses: actions/github-script@v7
50
+ with:
51
+ github-token: ${{ secrets.TERRAFORM_GITHUB_TOKEN }}
52
+ script: |
53
+ github.rest.repos.createDispatchEvent({
54
+ owner: context.repo.owner,
55
+ repo: 'terraform',
56
+ event_type: 'apply',
57
+
58
+ client_payload: {
59
+ path: '${{ env.terraform_module }}',
60
+ environment: '${{ inputs.environment }}'
61
+ }
62
+ });