railsforge 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +528 -0
- data/bin/railsforge +8 -0
- data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
- data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
- data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
- data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
- data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
- data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
- data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
- data/lib/railsforge/api_generator.rb +397 -0
- data/lib/railsforge/audit.rb +289 -0
- data/lib/railsforge/cli.rb +671 -0
- data/lib/railsforge/config.rb +181 -0
- data/lib/railsforge/database_analyzer.rb +300 -0
- data/lib/railsforge/doctor.rb +250 -0
- data/lib/railsforge/feature_generator.rb +560 -0
- data/lib/railsforge/generator.rb +313 -0
- data/lib/railsforge/generators/base_generator.rb +70 -0
- data/lib/railsforge/generators/demo_generator.rb +307 -0
- data/lib/railsforge/generators/devops_generator.rb +287 -0
- data/lib/railsforge/generators/monitoring_generator.rb +134 -0
- data/lib/railsforge/generators/service_generator.rb +122 -0
- data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
- data/lib/railsforge/generators/test_generator.rb +289 -0
- data/lib/railsforge/generators/view_component_generator.rb +169 -0
- data/lib/railsforge/graph.rb +270 -0
- data/lib/railsforge/loader.rb +56 -0
- data/lib/railsforge/mailer_generator.rb +191 -0
- data/lib/railsforge/plugins/plugin_loader.rb +60 -0
- data/lib/railsforge/plugins.rb +30 -0
- data/lib/railsforge/profiles/admin_app.yml +49 -0
- data/lib/railsforge/profiles/api_only.yml +47 -0
- data/lib/railsforge/profiles/blog.yml +47 -0
- data/lib/railsforge/profiles/standard.yml +44 -0
- data/lib/railsforge/profiles.rb +99 -0
- data/lib/railsforge/refactor_analyzer.rb +401 -0
- data/lib/railsforge/refactor_controller.rb +277 -0
- data/lib/railsforge/refactors/refactor_controller.rb +117 -0
- data/lib/railsforge/template_loader.rb +105 -0
- data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
- data/lib/railsforge/templates/v1/form/template.rb +28 -0
- data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
- data/lib/railsforge/templates/v1/job/template.rb +13 -0
- data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
- data/lib/railsforge/templates/v1/policy/template.rb +57 -0
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
- data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/query/template.rb +16 -0
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
- data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
- data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/service/template.rb +25 -0
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
- data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
- data/lib/railsforge/templates/v2/job/template.rb +49 -0
- data/lib/railsforge/templates/v2/query/template.rb +66 -0
- data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
- data/lib/railsforge/templates/v2/service/template.rb +71 -0
- data/lib/railsforge/templates/v3/job/template.rb +72 -0
- data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
- data/lib/railsforge/templates/v3/query/template.rb +115 -0
- data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
- data/lib/railsforge/templates/v3/service/template.rb +84 -0
- data/lib/railsforge/version.rb +5 -0
- data/lib/railsforge/wizard.rb +265 -0
- data/lib/railsforge/wizard_tui.rb +286 -0
- data/lib/railsforge.rb +13 -0
- metadata +216 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe <%= name.camelize %>Query do
|
|
4
|
+
let(:scope) { <%= name.camelize %>.all }
|
|
5
|
+
subject { described_class.new(scope: scope) }
|
|
6
|
+
|
|
7
|
+
describe '#call' do
|
|
8
|
+
it 'returns scope' do
|
|
9
|
+
expect(subject.call).to eq(scope)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Query class for <%= name %>
|
|
2
|
+
# Encapsulates database queries
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# result = <%= name.camelize %>Query.call
|
|
6
|
+
# result = <%= name.camelize %>Query.call(scope: User.active)
|
|
7
|
+
class <%= name.camelize %>Query
|
|
8
|
+
def initialize(scope: nil)
|
|
9
|
+
@scope = scope || <%= name.camelize %>.all
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
# TODO: Implement query logic
|
|
14
|
+
@scope
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe <%= name.camelize %>Serializer do
|
|
4
|
+
let(:<%= name.underscore %>) { <%= name.camelize %>.new<% attributes.each do |attr| %>
|
|
5
|
+
<%= name.underscore %>.<%= attr %> = 'test'<% end %> }
|
|
6
|
+
subject { described_class.new(<%= name.underscore %>) }
|
|
7
|
+
|
|
8
|
+
describe 'serialization' do
|
|
9
|
+
it 'includes id' do
|
|
10
|
+
expect(subject.as_json[:id]).to eq(<%= name.underscore %>.id)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Serializer class for <%= name %>
|
|
2
|
+
# Handles JSON serialization
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# render json: <%= name.camelize %>Serializer.new(<%= name.underscore %>)
|
|
6
|
+
class <%= name.camelize %>Serializer < ApplicationSerializer
|
|
7
|
+
attributes :id<% attributes.each do |attr| %>
|
|
8
|
+
:<%= attr %><% end %>
|
|
9
|
+
|
|
10
|
+
# TODO: Add custom methods
|
|
11
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Service class for <%= name %>
|
|
2
|
+
# Encapsulates business logic
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# result = <%= name.camelize %>Service.call(params)
|
|
6
|
+
#
|
|
7
|
+
# @example Basic service
|
|
8
|
+
# class CreateUserService
|
|
9
|
+
# def initialize(user_params)
|
|
10
|
+
# @user_params = user_params
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# def call
|
|
14
|
+
# User.create!(@user_params)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
class <%= name.camelize %>Service
|
|
18
|
+
def initialize(<%= name.underscore %>_params = {})
|
|
19
|
+
@params = <%= name.underscore %>_params
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
# TODO: Implement service logic
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Stimulus controller: <%= class_name %>
|
|
2
|
+
import { Controller } from "@hotwired/stimulus"
|
|
3
|
+
|
|
4
|
+
export default class <%= class_name %> extends Controller {
|
|
5
|
+
// Define static targets
|
|
6
|
+
static targets = ["output", "input"]
|
|
7
|
+
|
|
8
|
+
// Connect lifecycle
|
|
9
|
+
connect() {
|
|
10
|
+
console.log("<%= class_name %> connected")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Disconnect lifecycle
|
|
14
|
+
disconnect() {
|
|
15
|
+
console.log("<%= class_name %> disconnected")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Example action
|
|
19
|
+
// Usage: data-action="<%= underscore_name %>#greet"
|
|
20
|
+
greet() {
|
|
21
|
+
console.log("Hello from <%= class_name %>!")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Example: Handle input changes
|
|
25
|
+
// Usage: data-action="input-><%= underscore_name %>#handleInput"
|
|
26
|
+
handleInput(event) {
|
|
27
|
+
const value = event.target.value
|
|
28
|
+
console.log("Input value:", value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Example: Toggle visibility
|
|
32
|
+
toggle() {
|
|
33
|
+
this.outputTarget.classList.toggle("hidden")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ViewComponent template for <%= class_name %>
|
|
2
|
+
class <%= class_name %> < ViewComponent::Base
|
|
3
|
+
# Define component properties
|
|
4
|
+
# renders_maybe :some_child
|
|
5
|
+
|
|
6
|
+
# Initialize with data
|
|
7
|
+
# @param title [String] Component title
|
|
8
|
+
def initialize(title: "", classes: "")
|
|
9
|
+
@title = title
|
|
10
|
+
@classes = classes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check if title is present
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def title?
|
|
16
|
+
@title.present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# CSS classes for the component
|
|
20
|
+
# @return [String]
|
|
21
|
+
def classes
|
|
22
|
+
"component #{@classes}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Job template v2
|
|
2
|
+
# Enhanced job with error handling and callbacks
|
|
3
|
+
class <%= class_name %> < ApplicationJob
|
|
4
|
+
# Configure queue
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
# Retry configuration
|
|
8
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: 5
|
|
9
|
+
|
|
10
|
+
# Discard configuration
|
|
11
|
+
discard_on ActiveJob::DeserializationError
|
|
12
|
+
|
|
13
|
+
# Perform the job
|
|
14
|
+
# @param args [Array] Job arguments
|
|
15
|
+
def perform(*args)
|
|
16
|
+
# TODO: Implement job logic
|
|
17
|
+
Rails.logger.info "Executing #{self.class.name}"
|
|
18
|
+
|
|
19
|
+
# Example:
|
|
20
|
+
# SomeService.call(*args)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Called before job execution
|
|
24
|
+
# @return [void]
|
|
25
|
+
def before_perform
|
|
26
|
+
Rails.logger.info "Starting #{self.class.name}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Called after job execution
|
|
30
|
+
# @return [void]
|
|
31
|
+
def after_perform
|
|
32
|
+
Rails.logger.info "Completed #{self.class.name}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Called on job failure
|
|
36
|
+
# @param exception [StandardError] The exception that was raised
|
|
37
|
+
# @return [void]
|
|
38
|
+
def on_failure(exception)
|
|
39
|
+
Rails.logger.error "#{self.class.name} failed: #{exception.message}"
|
|
40
|
+
# Notify error tracking service (e.g., Sentry, Bugsnag)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Called when job is retried
|
|
44
|
+
# @param exception [StandardError] The exception that caused the retry
|
|
45
|
+
# @return [void]
|
|
46
|
+
def on_retry(exception)
|
|
47
|
+
Rails.logger.warn "#{self.class.name} retrying: #{exception.message}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Query object template v2
|
|
2
|
+
# Enhanced query with chainable interface
|
|
3
|
+
class <%= class_name %>
|
|
4
|
+
attr_reader :relation
|
|
5
|
+
|
|
6
|
+
def initialize(relation = nil)
|
|
7
|
+
@relation = relation || default_relation
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Chainable scope methods
|
|
11
|
+
def filter_by(**conditions)
|
|
12
|
+
conditions.each do |attribute, value|
|
|
13
|
+
@relation = @relation.where(attribute => value)
|
|
14
|
+
end
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def order_by(attribute, direction = :asc)
|
|
19
|
+
@relation = @relation.order(attribute => direction)
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def limit(count)
|
|
24
|
+
@relation = @relation.limit(count)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def offset(count)
|
|
29
|
+
@relation = @relation.offset(count)
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Execute the query
|
|
34
|
+
def call
|
|
35
|
+
@relation
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Convenience methods
|
|
39
|
+
def first
|
|
40
|
+
@relation.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def last
|
|
44
|
+
@relation.last
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def all
|
|
48
|
+
@relation.to_a
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def count
|
|
52
|
+
@relation.count
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def exists?
|
|
56
|
+
@relation.exists?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def default_relation
|
|
62
|
+
# TODO: Replace with actual model
|
|
63
|
+
# Example: Model.where(active: true)
|
|
64
|
+
raise NotImplementedError, "Override #default_relation in subclass"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# RSpec tests for <%= class_name %>
|
|
2
|
+
# Enhanced v2 testing with better patterns
|
|
3
|
+
RSpec.describe <%= class_name %> do
|
|
4
|
+
let(:params) { {} }
|
|
5
|
+
let(:service) { described_class.new(params) }
|
|
6
|
+
|
|
7
|
+
describe "#call" do
|
|
8
|
+
context "with valid params" do
|
|
9
|
+
it "returns a successful result" do
|
|
10
|
+
result = service.call
|
|
11
|
+
expect(result.success?).to be true
|
|
12
|
+
expect(result.data).to be_a(Hash)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context "with invalid params" do
|
|
17
|
+
let(:params) { { invalid: true } }
|
|
18
|
+
|
|
19
|
+
it "returns a failure result" do
|
|
20
|
+
result = service.call
|
|
21
|
+
expect(result.failure?).to be true
|
|
22
|
+
expect(result.errors).to be_an(Array)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe ".call" do
|
|
28
|
+
it "creates and calls the service" do
|
|
29
|
+
result = described_class.call({})
|
|
30
|
+
expect(result).to be_a(Result)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Service class template v2
|
|
2
|
+
# Enhanced service with better practices
|
|
3
|
+
class <%= class_name %>
|
|
4
|
+
# Initialize with dependencies
|
|
5
|
+
# @param [Hash] params - Parameters for the service
|
|
6
|
+
def initialize(params = {})
|
|
7
|
+
@params = params
|
|
8
|
+
@errors = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Execute the service logic
|
|
12
|
+
# @return [Result] Result object with success/failure
|
|
13
|
+
def call
|
|
14
|
+
return failure(["Validation failed"]) unless validate?
|
|
15
|
+
|
|
16
|
+
success(execute_operation)
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
failure([e.message])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Class-level call for convenience
|
|
22
|
+
# @param [Hash] params - Parameters for the service
|
|
23
|
+
# @return [Result] Result object
|
|
24
|
+
def self.call(params = {})
|
|
25
|
+
new(params).call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Validate inputs
|
|
31
|
+
# @return [Boolean] True if valid
|
|
32
|
+
def validate?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Execute the main operation
|
|
37
|
+
# @return [Object] Operation result
|
|
38
|
+
def execute_operation
|
|
39
|
+
# TODO: Implement service logic
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Helper method for success result
|
|
44
|
+
# @param [Object] data - Result data
|
|
45
|
+
# @return [Result]
|
|
46
|
+
def success(data)
|
|
47
|
+
Result.new(success: true, data: data, errors: [])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Helper method for failure result
|
|
51
|
+
# @param [Array<String>] errors - Error messages
|
|
52
|
+
# @return [Result]
|
|
53
|
+
def failure(errors)
|
|
54
|
+
Result.new(success: false, data: nil, errors: errors)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Result class for service responses
|
|
59
|
+
class Result
|
|
60
|
+
attr_reader :success, :data, :errors
|
|
61
|
+
|
|
62
|
+
def initialize(success:, data:, errors:)
|
|
63
|
+
@success = success
|
|
64
|
+
@data = data
|
|
65
|
+
@errors = errors
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def failure?
|
|
69
|
+
!success
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Job template v3
|
|
2
|
+
# Advanced job with retries, priority, and callbacks
|
|
3
|
+
class <%= class_name %>Job < ApplicationJob
|
|
4
|
+
# Queue configuration
|
|
5
|
+
queue_with_priority PRIORITIES[:default]
|
|
6
|
+
|
|
7
|
+
# Retry configuration
|
|
8
|
+
retry_on StandardError, wait: :exponential_backoff, attempts: 5
|
|
9
|
+
|
|
10
|
+
# Discard configuration for permanent failures
|
|
11
|
+
discard_on PermanentJobError do |job, error|
|
|
12
|
+
Rails.logger.error("Job #{job.job_id} permanently failed: #{error.message}")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Callback hooks
|
|
16
|
+
around_perform :instrument_job
|
|
17
|
+
before_perform :log_start
|
|
18
|
+
after_perform :log_complete
|
|
19
|
+
|
|
20
|
+
# Perform the job
|
|
21
|
+
# @param [Hash] args - Job arguments
|
|
22
|
+
def perform(*args)
|
|
23
|
+
# TODO: Implement job logic
|
|
24
|
+
# Example:
|
|
25
|
+
# user = User.find(args[:user_id])
|
|
26
|
+
# user.send_welcome_email
|
|
27
|
+
Rails.logger.info("Executing #{self.class.name}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Schedule for later
|
|
31
|
+
# @param [Integer] delay - Delay in seconds
|
|
32
|
+
def self.perform_in(delay, *args)
|
|
33
|
+
set(wait: delay seconds).perform_later(*args)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Schedule at specific time
|
|
37
|
+
# @param [DateTime] time - Scheduled time
|
|
38
|
+
def self.perform_at(time, *args)
|
|
39
|
+
set(wait_until: time).perform_later(*args)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Unique job - prevent duplicates
|
|
43
|
+
def self.unique_by(*attributes)
|
|
44
|
+
sidekiq_options unique: true,
|
|
45
|
+
unique_args: ->(args) { args }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Instrument the job for monitoring
|
|
51
|
+
def instrument_job
|
|
52
|
+
start_time = Time.now
|
|
53
|
+
Rails.logger.info("Starting #{self.class.name} at #{start_time}")
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
yield
|
|
57
|
+
ensure
|
|
58
|
+
duration = Time.now - start_time
|
|
59
|
+
Rails.logger.info("Completed #{self.class.name} in #{duration.round(2)}s")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Log job start
|
|
64
|
+
def log_start
|
|
65
|
+
Rails.logger.debug("Job #{job_id} starting")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Log job completion
|
|
69
|
+
def log_complete
|
|
70
|
+
Rails.logger.debug("Job #{job_id} completed")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Query spec template v3
|
|
2
|
+
require 'rails_helper'
|
|
3
|
+
|
|
4
|
+
RSpec.describe <%= class_name %> do
|
|
5
|
+
let(:query) { described_class.new(relation) }
|
|
6
|
+
let(:relation) { double('Relation') }
|
|
7
|
+
|
|
8
|
+
describe '#where' do
|
|
9
|
+
it 'adds conditions' do
|
|
10
|
+
expect(query.where(status: 'active')).to be_a(described_class)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#includes' do
|
|
15
|
+
it 'adds includes' do
|
|
16
|
+
expect(query.includes(:user)).to be_a(described_class)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#order' do
|
|
21
|
+
it 'adds order' do
|
|
22
|
+
expect(query.order(created_at: :desc)).to be_a(described_class)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#paginate' do
|
|
27
|
+
it 'sets limit and offset' do
|
|
28
|
+
paginated = query.paginate(page: 2, per_page: 10)
|
|
29
|
+
expect(paginated).to be_a(described_class)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '#call' do
|
|
34
|
+
it 'executes the query' do
|
|
35
|
+
allow(relation).to receive_message_chain(:where, :includes, :order, :to_a).and_return([])
|
|
36
|
+
expect(query.call).to eq([])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#count' do
|
|
41
|
+
it 'returns total count' do
|
|
42
|
+
allow(relation).to receive_message_chain(:where, :includes, :order, :count).and_return(10)
|
|
43
|
+
expect(query.count).to eq(10)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#pagination_info' do
|
|
48
|
+
it 'returns pagination metadata' do
|
|
49
|
+
allow(relation).to receive_message_chain(:where, :includes, :order, :count).and_return(25)
|
|
50
|
+
info = query.pagination_info(page: 1, per_page: 10)
|
|
51
|
+
expect(info[:total_pages]).to eq(3)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Query object template v3
|
|
2
|
+
# Advanced query with pagination and caching
|
|
3
|
+
class <%= class_name %>
|
|
4
|
+
# @return [ActiveRecord::Relation] relation
|
|
5
|
+
attr_reader :relation
|
|
6
|
+
|
|
7
|
+
def initialize(relation = default_relation)
|
|
8
|
+
@relation = relation
|
|
9
|
+
@conditions = []
|
|
10
|
+
@includes = []
|
|
11
|
+
@order = []
|
|
12
|
+
@limit_value = nil
|
|
13
|
+
@offset_value = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Add conditions
|
|
17
|
+
# @param [Hash] conditions - Query conditions
|
|
18
|
+
def where(conditions)
|
|
19
|
+
@conditions << conditions
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Eager load associations
|
|
24
|
+
# @param [Array<Symbol>] associations - Associations to include
|
|
25
|
+
def includes(*associations)
|
|
26
|
+
@includes << associations
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Order results
|
|
31
|
+
# @param [Hash] order_spec - Order specification
|
|
32
|
+
def order(order_spec)
|
|
33
|
+
@order << order_spec
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Paginate results
|
|
38
|
+
# @param [Integer] page - Page number (1-indexed)
|
|
39
|
+
# @param [Integer] per_page - Items per page
|
|
40
|
+
def paginate(page: 1, per_page: 25)
|
|
41
|
+
@limit_value = per_page
|
|
42
|
+
@offset_value = (page - 1) * per_page
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Cache results
|
|
47
|
+
# @param [Integer] duration - Cache duration in seconds
|
|
48
|
+
def cached(duration: 5.minutes)
|
|
49
|
+
cache_key = generate_cache_key
|
|
50
|
+
Rails.cache.fetch(cache_key, expires_in: duration) do
|
|
51
|
+
to_a
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Execute query and return results
|
|
56
|
+
def call
|
|
57
|
+
build_relation.to_a
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get total count
|
|
61
|
+
def count
|
|
62
|
+
build_relation.count
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Pagination metadata
|
|
66
|
+
def pagination_info(page: 1, per_page: 25)
|
|
67
|
+
total = count
|
|
68
|
+
{
|
|
69
|
+
current_page: page,
|
|
70
|
+
per_page: per_page,
|
|
71
|
+
total_count: total,
|
|
72
|
+
total_pages: (total.to_f / per_page).ceil,
|
|
73
|
+
has_next_page: (page * per_page) < total,
|
|
74
|
+
has_previous_page: page > 1
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Reset to default
|
|
79
|
+
def reset
|
|
80
|
+
@conditions = []
|
|
81
|
+
@includes = []
|
|
82
|
+
@order = []
|
|
83
|
+
@limit_value = nil
|
|
84
|
+
@offset_value = nil
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def default_relation
|
|
91
|
+
raise NotImplementedError, "Override default_relation in subclass"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_relation
|
|
95
|
+
result = @relation
|
|
96
|
+
|
|
97
|
+
@conditions.each { |cond| result = result.where(cond) }
|
|
98
|
+
@includes.flatten.each { |inc| result = result.includes(inc) }
|
|
99
|
+
@order.each { |ord| result = result.order(ord) }
|
|
100
|
+
result = result.limit(@limit_value) if @limit_value
|
|
101
|
+
result = result.offset(@offset_value) if @offset_value
|
|
102
|
+
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def generate_cache_key
|
|
107
|
+
{
|
|
108
|
+
conditions: @conditions,
|
|
109
|
+
includes: @includes,
|
|
110
|
+
order: @order,
|
|
111
|
+
limit: @limit_value,
|
|
112
|
+
offset: @offset_value
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Service spec template v3
|
|
2
|
+
require 'rails_helper'
|
|
3
|
+
|
|
4
|
+
RSpec.describe <%= class_name %> do
|
|
5
|
+
let(:service) { described_class.new(params) }
|
|
6
|
+
let(:params) { { name: 'Test' } }
|
|
7
|
+
|
|
8
|
+
describe '#call' do
|
|
9
|
+
context 'with valid params' do
|
|
10
|
+
it 'returns success' do
|
|
11
|
+
result = service.call
|
|
12
|
+
expect(result).to be_success
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'contains data in result' do
|
|
16
|
+
result = service.call
|
|
17
|
+
expect(result.value!).to have_key(:data)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'includes metadata' do
|
|
21
|
+
result = service.call
|
|
22
|
+
expect(result.value!).to have_key(:metadata)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'with invalid params' do
|
|
27
|
+
let(:params) { {} }
|
|
28
|
+
|
|
29
|
+
it 'returns failure' do
|
|
30
|
+
result = service.call
|
|
31
|
+
expect(result).to be_failure
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '.call' do
|
|
37
|
+
it 'works as class method' do
|
|
38
|
+
result = described_class.call(name: 'Test')
|
|
39
|
+
expect(result).to be_success
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#then' do
|
|
44
|
+
let(:next_service) { double('NextService', new: double(call: Success({}))) }
|
|
45
|
+
|
|
46
|
+
it 'chains to another service' do
|
|
47
|
+
result = service.then(next_service, extra: 'param')
|
|
48
|
+
expect(result).to be_a(Dry::Monads::Result)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|