better_service 2.0.0 → 2.1.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 +4 -4
- data/LICENSE +2 -0
- data/README.md +98 -45
- data/Rakefile +7 -209
- data/config/locales/better_service.en.yml +15 -0
- data/lib/better_service/cache_service.rb +4 -4
- data/lib/better_service/concerns/instrumentation.rb +59 -14
- data/lib/better_service/concerns/serviceable/authorizable.rb +1 -1
- data/lib/better_service/concerns/serviceable/messageable.rb +70 -1
- data/lib/better_service/concerns/serviceable/repository_aware.rb +8 -3
- data/lib/better_service/concerns/workflowable/callbacks.rb +27 -27
- data/lib/better_service/concerns/workflowable/step.rb +39 -5
- data/lib/better_service/errors/better_service_error.rb +4 -0
- data/lib/better_service/errors/runtime/authorization_error.rb +4 -1
- data/lib/better_service/errors/runtime/database_error.rb +4 -1
- data/lib/better_service/errors/runtime/execution_error.rb +4 -1
- data/lib/better_service/errors/runtime/invalid_result_error.rb +28 -0
- data/lib/better_service/errors/runtime/resource_not_found_error.rb +4 -1
- data/lib/better_service/errors/runtime/validation_error.rb +4 -1
- data/lib/better_service/repository/base_repository.rb +1 -1
- data/lib/better_service/result.rb +110 -0
- data/lib/better_service/services/base.rb +216 -57
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/branch_group.rb +1 -1
- data/lib/better_service.rb +1 -6
- data/lib/generators/serviceable/action_generator.rb +11 -0
- data/lib/generators/serviceable/base_generator.rb +109 -0
- data/lib/generators/serviceable/create_generator.rb +11 -0
- data/lib/generators/serviceable/destroy_generator.rb +11 -0
- data/lib/generators/serviceable/index_generator.rb +11 -0
- data/lib/generators/serviceable/scaffold_generator.rb +29 -7
- data/lib/generators/serviceable/show_generator.rb +11 -0
- data/lib/generators/serviceable/templates/action_service.rb.tt +8 -3
- data/lib/generators/serviceable/templates/base_locale.en.yml.tt +53 -0
- data/lib/generators/serviceable/templates/base_service.rb.tt +78 -0
- data/lib/generators/serviceable/templates/base_service_test.rb.tt +64 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +29 -18
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +16 -29
- data/lib/generators/serviceable/templates/index_service.rb.tt +16 -34
- data/lib/generators/serviceable/templates/repository.rb.tt +76 -0
- data/lib/generators/serviceable/templates/repository_test.rb.tt +124 -0
- data/lib/generators/serviceable/templates/show_service.rb.tt +10 -38
- data/lib/generators/serviceable/templates/update_service.rb.tt +24 -38
- data/lib/generators/serviceable/update_generator.rb +11 -0
- metadata +13 -12
- data/lib/better_service/concerns/serviceable/viewable.rb +0 -33
- data/lib/better_service/services/action_service.rb +0 -60
- data/lib/better_service/services/create_service.rb +0 -63
- data/lib/better_service/services/destroy_service.rb +0 -60
- data/lib/better_service/services/index_service.rb +0 -56
- data/lib/better_service/services/show_service.rb +0 -44
- data/lib/better_service/services/update_service.rb +0 -61
|
@@ -9,6 +9,9 @@ module Serviceable
|
|
|
9
9
|
|
|
10
10
|
desc "Generate a Show service for displaying a single resource"
|
|
11
11
|
|
|
12
|
+
class_option :base_class, type: :string, default: nil,
|
|
13
|
+
desc: "Custom base class to inherit from (e.g., Articles::BaseService)"
|
|
14
|
+
|
|
12
15
|
def create_service_file
|
|
13
16
|
template "show_service.rb.tt", File.join("app/services", class_path, "#{file_name}/show_service.rb")
|
|
14
17
|
end
|
|
@@ -22,6 +25,14 @@ module Serviceable
|
|
|
22
25
|
def service_class_name
|
|
23
26
|
"#{class_name}::ShowService"
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
def parent_class
|
|
30
|
+
options[:base_class] || "BetterService::Services::Base"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def using_base_service?
|
|
34
|
+
options[:base_class].present?
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
<% module_namespacing do -%>
|
|
4
|
-
class <%= class_name %>::<%= action_name.camelize %>Service <
|
|
5
|
-
|
|
4
|
+
class <%= class_name %>::<%= action_name.camelize %>Service < <%= parent_class %>
|
|
5
|
+
# Action name for metadata
|
|
6
|
+
performed_action :<%= action_name %>
|
|
6
7
|
|
|
7
8
|
# Schema for validating params
|
|
8
9
|
schema do
|
|
@@ -23,7 +24,11 @@ class <%= class_name %>::<%= action_name.camelize %>Service < BetterService::Ser
|
|
|
23
24
|
|
|
24
25
|
# Phase 1: Search - Load the resource
|
|
25
26
|
search_with do
|
|
27
|
+
<% if using_base_service? -%>
|
|
28
|
+
{ resource: <%= singular_table_name %>_repository.find(params[:id]) }
|
|
29
|
+
<% else -%>
|
|
26
30
|
{ resource: user.<%= plural_table_name %>.find(params[:id]) }
|
|
31
|
+
<% end -%>
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
# Phase 2: Process - Perform the action
|
|
@@ -36,7 +41,7 @@ class <%= class_name %>::<%= action_name.camelize %>Service < BetterService::Ser
|
|
|
36
41
|
|
|
37
42
|
# Phase 4: Respond - Format response (optional override)
|
|
38
43
|
respond_with do |data|
|
|
39
|
-
success_result("<%=
|
|
44
|
+
success_result(message("<%= action_name %>.success"), data)
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
<% end -%>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
en:
|
|
2
|
+
<%= file_name %>:
|
|
3
|
+
services:
|
|
4
|
+
# Index service messages
|
|
5
|
+
index:
|
|
6
|
+
success: "<%= human_name.pluralize %> loaded successfully"
|
|
7
|
+
error: "Error loading <%= human_name.downcase.pluralize %>"
|
|
8
|
+
empty: "No <%= human_name.downcase.pluralize %> found"
|
|
9
|
+
|
|
10
|
+
# Show service messages
|
|
11
|
+
show:
|
|
12
|
+
success: "<%= human_name %> loaded successfully"
|
|
13
|
+
error: "Error loading <%= human_name.downcase %>"
|
|
14
|
+
not_found: "<%= human_name %> not found"
|
|
15
|
+
|
|
16
|
+
# Create service messages
|
|
17
|
+
create:
|
|
18
|
+
success: "<%= human_name %> created successfully"
|
|
19
|
+
error: "Error creating <%= human_name.downcase %>"
|
|
20
|
+
validation_failed: "Could not create <%= human_name.downcase %> due to validation errors"
|
|
21
|
+
|
|
22
|
+
# Update service messages
|
|
23
|
+
update:
|
|
24
|
+
success: "<%= human_name %> updated successfully"
|
|
25
|
+
error: "Error updating <%= human_name.downcase %>"
|
|
26
|
+
validation_failed: "Could not update <%= human_name.downcase %> due to validation errors"
|
|
27
|
+
not_found: "<%= human_name %> not found"
|
|
28
|
+
|
|
29
|
+
# Destroy service messages
|
|
30
|
+
destroy:
|
|
31
|
+
success: "<%= human_name %> deleted successfully"
|
|
32
|
+
error: "Error deleting <%= human_name.downcase %>"
|
|
33
|
+
not_found: "<%= human_name %> not found"
|
|
34
|
+
cannot_destroy: "Cannot delete this <%= human_name.downcase %>"
|
|
35
|
+
|
|
36
|
+
# Common messages (shared across services)
|
|
37
|
+
common:
|
|
38
|
+
success: "Operation completed successfully"
|
|
39
|
+
error: "An error occurred"
|
|
40
|
+
not_found: "<%= human_name %> not found"
|
|
41
|
+
not_authorized: "You are not authorized to perform this action"
|
|
42
|
+
invalid_params: "Invalid parameters provided"
|
|
43
|
+
already_exists: "<%= human_name %> already exists"
|
|
44
|
+
|
|
45
|
+
# Add custom action messages here:
|
|
46
|
+
# publish:
|
|
47
|
+
# success: "<%= human_name %> published successfully"
|
|
48
|
+
# error: "Error publishing <%= human_name.downcase %>"
|
|
49
|
+
# already_published: "<%= human_name %> is already published"
|
|
50
|
+
#
|
|
51
|
+
# archive:
|
|
52
|
+
# success: "<%= human_name %> archived successfully"
|
|
53
|
+
# error: "Error archiving <%= human_name.downcase %>"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
# BaseService for <%= class_name %> resource operations.
|
|
5
|
+
#
|
|
6
|
+
# Provides centralized configuration for all <%= class_name %> services:
|
|
7
|
+
# - Repository access via RepositoryAware concern
|
|
8
|
+
# - I18n messages namespace
|
|
9
|
+
# - Cache invalidation contexts
|
|
10
|
+
# - Presenter configuration
|
|
11
|
+
#
|
|
12
|
+
# All <%= class_name %> services should inherit from this class:
|
|
13
|
+
# class <%= class_name %>::IndexService < <%= class_name %>::BaseService
|
|
14
|
+
# class <%= class_name %>::CreateService < <%= class_name %>::BaseService
|
|
15
|
+
#
|
|
16
|
+
# @example Usage
|
|
17
|
+
# class <%= class_name %>::IndexService < <%= class_name %>::BaseService
|
|
18
|
+
# schema do
|
|
19
|
+
# optional(:page).filled(:integer, gteq?: 1)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# search_with do
|
|
23
|
+
# { items: <%= singular_table_name %>_repository.all.to_a }
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
class <%= class_name %>::BaseService < BetterService::Services::Base
|
|
28
|
+
include BetterService::Concerns::Serviceable::RepositoryAware
|
|
29
|
+
|
|
30
|
+
# I18n messages namespace - messages are loaded from:
|
|
31
|
+
# config/locales/<%= file_name %>_services.en.yml
|
|
32
|
+
messages_namespace :<%= file_name %>
|
|
33
|
+
|
|
34
|
+
# Cache contexts for automatic invalidation
|
|
35
|
+
# Create/Update/Destroy services will invalidate these contexts
|
|
36
|
+
cache_contexts [:<%= plural_table_name %>]
|
|
37
|
+
|
|
38
|
+
<% unless options[:skip_presenter] -%>
|
|
39
|
+
# Presenter for transforming data in responses
|
|
40
|
+
# Generate with: rails generate better_service:presenter <%= class_name %>
|
|
41
|
+
# presenter <%= class_name %>Presenter
|
|
42
|
+
# presenter_options do
|
|
43
|
+
# { current_user: user }
|
|
44
|
+
# end
|
|
45
|
+
|
|
46
|
+
<% end -%>
|
|
47
|
+
<% unless options[:skip_repository] -%>
|
|
48
|
+
# Repository declaration - creates <%= singular_table_name %>_repository method
|
|
49
|
+
repository :<%= singular_table_name %>
|
|
50
|
+
|
|
51
|
+
# Add more repositories as needed:
|
|
52
|
+
# repository :user
|
|
53
|
+
# repository :category, class_name: "Categories::CategoryRepository"
|
|
54
|
+
|
|
55
|
+
<% end -%>
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Override to provide default error message for this resource
|
|
59
|
+
def default_error_message
|
|
60
|
+
message("common.error")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Override to provide default success message for this resource
|
|
64
|
+
def default_success_message
|
|
65
|
+
message("common.success")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Add shared helper methods for all <%= class_name %> services here:
|
|
69
|
+
#
|
|
70
|
+
# def find_<%= singular_table_name %>(id)
|
|
71
|
+
# <%= singular_table_name %>_repository.find(id)
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# def <%= singular_table_name %>_authorized?(record)
|
|
75
|
+
# record.user_id == user.id
|
|
76
|
+
# end
|
|
77
|
+
end
|
|
78
|
+
<% end -%>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>::BaseServiceTest < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@user = users(:one) # Adjust fixture name as needed
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Test that the base service cannot be called directly
|
|
12
|
+
# (it requires subclass implementation)
|
|
13
|
+
test "base service requires schema definition" do
|
|
14
|
+
assert_raises(BetterService::Errors::Configuration::SchemaRequiredError) do
|
|
15
|
+
<%= class_name %>::BaseService.new(@user, params: {})
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test "messages_namespace is configured" do
|
|
20
|
+
assert_equal :<%= file_name %>, <%= class_name %>::BaseService._messages_namespace
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "cache_contexts is configured" do
|
|
24
|
+
assert_includes <%= class_name %>::BaseService._cache_contexts, :<%= plural_table_name %>
|
|
25
|
+
end
|
|
26
|
+
<% unless options[:skip_repository] -%>
|
|
27
|
+
|
|
28
|
+
# Repository tests
|
|
29
|
+
# Note: These tests verify the repository declaration works
|
|
30
|
+
# Actual repository functionality is tested in repository_test.rb
|
|
31
|
+
|
|
32
|
+
test "repository is accessible via method" do
|
|
33
|
+
# Create a concrete subclass to test repository access
|
|
34
|
+
test_service_class = Class.new(<%= class_name %>::BaseService) do
|
|
35
|
+
schema { optional(:id).filled }
|
|
36
|
+
|
|
37
|
+
def test_repository_access
|
|
38
|
+
<%= singular_table_name %>_repository
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
service = test_service_class.new(@user, params: {})
|
|
43
|
+
repo = service.send(:test_repository_access)
|
|
44
|
+
|
|
45
|
+
assert_instance_of <%= class_name %>Repository, repo
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test "repository is memoized" do
|
|
49
|
+
test_service_class = Class.new(<%= class_name %>::BaseService) do
|
|
50
|
+
schema { optional(:id).filled }
|
|
51
|
+
|
|
52
|
+
def test_repository_memoization
|
|
53
|
+
[<%= singular_table_name %>_repository, <%= singular_table_name %>_repository]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
service = test_service_class.new(@user, params: {})
|
|
58
|
+
repos = service.send(:test_repository_memoization)
|
|
59
|
+
|
|
60
|
+
assert_same repos[0], repos[1], "Repository should be memoized"
|
|
61
|
+
end
|
|
62
|
+
<% end -%>
|
|
63
|
+
end
|
|
64
|
+
<% end -%>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
<% module_namespacing do -%>
|
|
4
|
-
class <%= class_name %>::CreateService <
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
class <%= class_name %>::CreateService < <%= parent_class %>
|
|
5
|
+
# Action name for metadata
|
|
6
|
+
performed_action :created
|
|
7
|
+
|
|
8
|
+
# Enable transaction wrapping
|
|
9
|
+
with_transaction true
|
|
8
10
|
|
|
9
11
|
# Schema for validating params
|
|
10
12
|
schema do
|
|
@@ -20,24 +22,33 @@ class <%= class_name %>::CreateService < BetterService::Services::CreateService
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
# Phase 2: Process - Create the resource (runs in transaction)
|
|
25
|
+
# Uses save (not save!) to allow graceful validation handling
|
|
23
26
|
process_with do |data|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
<% if using_base_service? -%>
|
|
28
|
+
record = <%= singular_table_name %>_repository.build(params)
|
|
29
|
+
|
|
30
|
+
if record.save
|
|
31
|
+
{ object: record }
|
|
32
|
+
else
|
|
33
|
+
failure_for(record)
|
|
34
|
+
end
|
|
35
|
+
<% else -%>
|
|
36
|
+
<%= file_name %> = user.<%= plural_table_name %>.build(params)
|
|
37
|
+
|
|
38
|
+
if <%= file_name %>.save
|
|
39
|
+
{ object: <%= file_name %> }
|
|
40
|
+
else
|
|
41
|
+
failure_for(<%= file_name %>)
|
|
42
|
+
end
|
|
43
|
+
<% end -%>
|
|
26
44
|
end
|
|
27
45
|
|
|
28
|
-
# Phase 4: Respond - Format response
|
|
29
|
-
# Uses I18n message helper with fallback to default messages
|
|
30
|
-
#
|
|
31
|
-
# To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
|
|
32
|
-
# en:
|
|
33
|
-
# <%= plural_table_name %>:
|
|
34
|
-
# services:
|
|
35
|
-
# create:
|
|
36
|
-
# success: "<%= human_name %> created successfully!"
|
|
37
|
-
#
|
|
38
|
-
# Then configure namespace: messages_namespace :<%= plural_table_name %>
|
|
46
|
+
# Phase 4: Respond - Format response
|
|
39
47
|
respond_with do |data|
|
|
40
|
-
|
|
48
|
+
# Pass through failure responses
|
|
49
|
+
return data if data[:success] == false
|
|
50
|
+
|
|
51
|
+
success_for(data[:object], message("create.success"))
|
|
41
52
|
end
|
|
42
53
|
end
|
|
43
54
|
<% end -%>
|
|
@@ -1,50 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
<% module_namespacing do -%>
|
|
4
|
-
class <%= class_name %>::DestroyService <
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
class <%= class_name %>::DestroyService < <%= parent_class %>
|
|
5
|
+
# Action name for metadata
|
|
6
|
+
performed_action :destroyed
|
|
7
|
+
|
|
8
|
+
# Enable transaction wrapping
|
|
9
|
+
with_transaction true
|
|
8
10
|
|
|
9
11
|
# Schema for validating params
|
|
10
12
|
schema do
|
|
11
13
|
required(:id).filled
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
# Authorization (optional) - Runs BEFORE search phase
|
|
15
|
-
# Uncomment and customize for your needs:
|
|
16
|
-
#
|
|
17
|
-
# authorize_with do
|
|
18
|
-
# # Example: Check ownership or admin role
|
|
19
|
-
# <%= file_name %> = <%= class_name %>.find(params[:id])
|
|
20
|
-
# user.admin? || <%= file_name %>.user_id == user.id
|
|
21
|
-
# end
|
|
22
|
-
|
|
23
16
|
# Phase 1: Search - Load the resource
|
|
24
17
|
search_with do
|
|
25
|
-
|
|
18
|
+
<% if using_base_service? -%>
|
|
19
|
+
{ object: <%= singular_table_name %>_repository.find(params[:id]) }
|
|
20
|
+
<% else -%>
|
|
21
|
+
{ object: user.<%= plural_table_name %>.find(params[:id]) }
|
|
22
|
+
<% end -%>
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
# Phase 2: Process - Delete the resource (runs in transaction)
|
|
29
26
|
process_with do |data|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
27
|
+
record = data[:object]
|
|
28
|
+
record.destroy!
|
|
29
|
+
{ object: record }
|
|
33
30
|
end
|
|
34
31
|
|
|
35
|
-
# Phase 4: Respond - Format response
|
|
36
|
-
# Uses I18n message helper with fallback to default messages
|
|
37
|
-
#
|
|
38
|
-
# To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
|
|
39
|
-
# en:
|
|
40
|
-
# <%= plural_table_name %>:
|
|
41
|
-
# services:
|
|
42
|
-
# destroy:
|
|
43
|
-
# success: "<%= human_name %> deleted successfully!"
|
|
44
|
-
#
|
|
45
|
-
# Then configure namespace: messages_namespace :<%= plural_table_name %>
|
|
32
|
+
# Phase 4: Respond - Format response
|
|
46
33
|
respond_with do |data|
|
|
47
|
-
|
|
34
|
+
success_for(data[:object], message("destroy.success"))
|
|
48
35
|
end
|
|
49
36
|
end
|
|
50
37
|
<% end -%>
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
<% module_namespacing do -%>
|
|
4
|
-
class <%= class_name %>::IndexService <
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
# presenter <%= class_name %>Presenter
|
|
9
|
-
# presenter_options do
|
|
10
|
-
# { current_user: user }
|
|
11
|
-
# end
|
|
4
|
+
class <%= class_name %>::IndexService < <%= parent_class %>
|
|
5
|
+
# Action name for metadata
|
|
6
|
+
performed_action :listed
|
|
12
7
|
|
|
13
8
|
# Schema for validating params
|
|
14
9
|
schema do
|
|
@@ -19,21 +14,29 @@ class <%= class_name %>::IndexService < BetterService::Services::IndexService
|
|
|
19
14
|
|
|
20
15
|
# Phase 1: Search - Load raw data
|
|
21
16
|
search_with do
|
|
17
|
+
<% if using_base_service? -%>
|
|
18
|
+
# Use repository for data access
|
|
19
|
+
predicates = {}
|
|
20
|
+
predicates[:search] = params[:search] if params[:search].present?
|
|
21
|
+
|
|
22
|
+
{ object: <%= singular_table_name %>_repository.search(predicates, page: params[:page] || 1, per_page: params[:per_page] || 25).to_a }
|
|
23
|
+
<% else -%>
|
|
22
24
|
<%= plural_table_name %> = user.<%= plural_table_name %>
|
|
23
25
|
<%= plural_table_name %> = <%= plural_table_name %>.where("title LIKE ?", "%#{params[:search]}%") if params[:search].present?
|
|
24
26
|
# Add pagination if needed (e.g., with Kaminari or Pagy)
|
|
25
27
|
# <%= plural_table_name %> = <%= plural_table_name %>.page(params[:page]).per(params[:per_page] || 25)
|
|
26
28
|
|
|
27
|
-
{
|
|
29
|
+
{ object: <%= plural_table_name %>.to_a }
|
|
30
|
+
<% end -%>
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
# Phase 2: Process - Transform and aggregate data
|
|
31
34
|
process_with do |data|
|
|
32
35
|
{
|
|
33
|
-
|
|
36
|
+
object: data[:object],
|
|
34
37
|
metadata: {
|
|
35
38
|
stats: {
|
|
36
|
-
total: data[:
|
|
39
|
+
total: data[:object].count
|
|
37
40
|
},
|
|
38
41
|
pagination: {
|
|
39
42
|
page: params[:page] || 1,
|
|
@@ -43,30 +46,9 @@ class <%= class_name %>::IndexService < BetterService::Services::IndexService
|
|
|
43
46
|
}
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
# Phase 4: Respond - Format response
|
|
47
|
-
# Uses I18n message helper with fallback to default messages
|
|
48
|
-
#
|
|
49
|
-
# To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
|
|
50
|
-
# en:
|
|
51
|
-
# <%= plural_table_name %>:
|
|
52
|
-
# services:
|
|
53
|
-
# index:
|
|
54
|
-
# success: "<%= plural_table_name.titleize %> loaded successfully!"
|
|
55
|
-
#
|
|
56
|
-
# Then configure namespace: messages_namespace :<%= plural_table_name %>
|
|
49
|
+
# Phase 4: Respond - Format response
|
|
57
50
|
respond_with do |data|
|
|
58
|
-
|
|
51
|
+
success_for(data[:object], message("index.success")).merge(metadata: data[:metadata])
|
|
59
52
|
end
|
|
60
|
-
|
|
61
|
-
# Phase 5: Viewer - UI configuration (optional)
|
|
62
|
-
# viewer do |processed, transformed, result|
|
|
63
|
-
# {
|
|
64
|
-
# page_title: "<%= plural_table_name.titleize %>",
|
|
65
|
-
# breadcrumbs: [
|
|
66
|
-
# { label: "Home", url: "/" },
|
|
67
|
-
# { label: "<%= plural_table_name.titleize %>", url: "/<%= plural_table_name %>" }
|
|
68
|
-
# ]
|
|
69
|
-
# }
|
|
70
|
-
# end
|
|
71
53
|
end
|
|
72
54
|
<% end -%>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
# Repository for <%= class_name %> model data access.
|
|
5
|
+
#
|
|
6
|
+
# Provides a clean abstraction layer between services and ActiveRecord.
|
|
7
|
+
# The model class is automatically inferred from the repository name:
|
|
8
|
+
# <%= class_name %>Repository -> <%= class_name %>
|
|
9
|
+
#
|
|
10
|
+
# Inherited methods from BaseRepository:
|
|
11
|
+
# - find(id), find_by(attributes), where(conditions)
|
|
12
|
+
# - create(attributes), create!(attributes)
|
|
13
|
+
# - update(record, attributes), update!(record, attributes)
|
|
14
|
+
# - destroy(record), destroy!(record)
|
|
15
|
+
# - search(predicates, page:, per_page:, includes:, order:)
|
|
16
|
+
# - all, count, exists?
|
|
17
|
+
#
|
|
18
|
+
# @example Basic usage
|
|
19
|
+
# repo = <%= class_name %>Repository.new
|
|
20
|
+
# repo.find(1)
|
|
21
|
+
# repo.search({ status_eq: 'active' }, page: 1, per_page: 20)
|
|
22
|
+
#
|
|
23
|
+
# @example In services (via RepositoryAware)
|
|
24
|
+
# class <%= class_name %>::IndexService < <%= class_name %>::BaseService
|
|
25
|
+
# search_with do
|
|
26
|
+
# { items: <%= singular_table_name %>_repository.active.recent.to_a }
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class <%= class_name %>Repository < BetterService::Repository::BaseRepository
|
|
31
|
+
# Model is inferred automatically: <%= class_name %>Repository -> <%= class_name %>
|
|
32
|
+
# Override with explicit model if needed:
|
|
33
|
+
# def initialize(model_class = <%= class_name %>)
|
|
34
|
+
# super
|
|
35
|
+
# end
|
|
36
|
+
|
|
37
|
+
# Add custom repository methods below:
|
|
38
|
+
|
|
39
|
+
# @example Scope methods
|
|
40
|
+
# def active
|
|
41
|
+
# where(active: true)
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# def inactive
|
|
45
|
+
# where(active: false)
|
|
46
|
+
# end
|
|
47
|
+
|
|
48
|
+
# @example Ownership methods
|
|
49
|
+
# def for_user(user)
|
|
50
|
+
# where(user_id: user.id)
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# def for_organization(org)
|
|
54
|
+
# where(organization_id: org.id)
|
|
55
|
+
# end
|
|
56
|
+
|
|
57
|
+
# @example Ordering methods
|
|
58
|
+
# def recent(limit = 10)
|
|
59
|
+
# model.order(created_at: :desc).limit(limit)
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# def by_name
|
|
63
|
+
# model.order(:name)
|
|
64
|
+
# end
|
|
65
|
+
|
|
66
|
+
# @example Complex queries
|
|
67
|
+
# def with_associations
|
|
68
|
+
# model.includes(:user, :category)
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# def published_today
|
|
72
|
+
# where(published: true)
|
|
73
|
+
# .where("published_at >= ?", Time.current.beginning_of_day)
|
|
74
|
+
# end
|
|
75
|
+
end
|
|
76
|
+
<% end -%>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>RepositoryTest < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@repository = <%= class_name %>Repository.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Model inference tests
|
|
12
|
+
test "repository infers model class correctly" do
|
|
13
|
+
assert_equal <%= class_name %>, @repository.model
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Basic CRUD operation tests
|
|
17
|
+
# Uncomment and adjust based on your fixtures/factories
|
|
18
|
+
|
|
19
|
+
# test "all returns collection" do
|
|
20
|
+
# records = @repository.all
|
|
21
|
+
# assert_respond_to records, :each
|
|
22
|
+
# assert_kind_of ActiveRecord::Relation, records
|
|
23
|
+
# end
|
|
24
|
+
|
|
25
|
+
# test "find returns record by id" do
|
|
26
|
+
# record = <%= plural_table_name %>(:one)
|
|
27
|
+
# found = @repository.find(record.id)
|
|
28
|
+
# assert_equal record, found
|
|
29
|
+
# end
|
|
30
|
+
|
|
31
|
+
# test "find raises for non-existent record" do
|
|
32
|
+
# assert_raises(ActiveRecord::RecordNotFound) do
|
|
33
|
+
# @repository.find(-1)
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
|
|
37
|
+
# test "find_by returns matching record" do
|
|
38
|
+
# record = <%= plural_table_name %>(:one)
|
|
39
|
+
# found = @repository.find_by(id: record.id)
|
|
40
|
+
# assert_equal record, found
|
|
41
|
+
# end
|
|
42
|
+
|
|
43
|
+
# test "find_by returns nil for no match" do
|
|
44
|
+
# found = @repository.find_by(id: -1)
|
|
45
|
+
# assert_nil found
|
|
46
|
+
# end
|
|
47
|
+
|
|
48
|
+
# test "where returns filtered collection" do
|
|
49
|
+
# records = @repository.where(active: true)
|
|
50
|
+
# assert_kind_of ActiveRecord::Relation, records
|
|
51
|
+
# end
|
|
52
|
+
|
|
53
|
+
# test "count returns number of records" do
|
|
54
|
+
# count = @repository.count
|
|
55
|
+
# assert_kind_of Integer, count
|
|
56
|
+
# end
|
|
57
|
+
|
|
58
|
+
# test "exists? returns boolean" do
|
|
59
|
+
# assert_includes [true, false], @repository.exists?(id: 1)
|
|
60
|
+
# end
|
|
61
|
+
|
|
62
|
+
# Create operation tests
|
|
63
|
+
|
|
64
|
+
# test "create persists new record" do
|
|
65
|
+
# assert_difference '<%= class_name %>.count' do
|
|
66
|
+
# @repository.create(valid_attributes)
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
|
|
70
|
+
# test "create! raises on invalid attributes" do
|
|
71
|
+
# assert_raises(ActiveRecord::RecordInvalid) do
|
|
72
|
+
# @repository.create!(invalid_attributes)
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
|
|
76
|
+
# Update operation tests
|
|
77
|
+
|
|
78
|
+
# test "update modifies record" do
|
|
79
|
+
# record = <%= plural_table_name %>(:one)
|
|
80
|
+
# @repository.update(record, name: "Updated Name")
|
|
81
|
+
# record.reload
|
|
82
|
+
# assert_equal "Updated Name", record.name
|
|
83
|
+
# end
|
|
84
|
+
|
|
85
|
+
# Destroy operation tests
|
|
86
|
+
|
|
87
|
+
# test "destroy removes record" do
|
|
88
|
+
# record = <%= plural_table_name %>(:one)
|
|
89
|
+
# assert_difference '<%= class_name %>.count', -1 do
|
|
90
|
+
# @repository.destroy(record)
|
|
91
|
+
# end
|
|
92
|
+
# end
|
|
93
|
+
|
|
94
|
+
# Search operation tests (if using predicates)
|
|
95
|
+
|
|
96
|
+
# test "search with predicates returns filtered results" do
|
|
97
|
+
# results = @repository.search({ active_eq: true }, limit: nil)
|
|
98
|
+
# assert results.all?(&:active)
|
|
99
|
+
# end
|
|
100
|
+
|
|
101
|
+
# test "search with pagination" do
|
|
102
|
+
# results = @repository.search({}, page: 1, per_page: 10)
|
|
103
|
+
# assert results.size <= 10
|
|
104
|
+
# end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Define valid attributes for create tests
|
|
109
|
+
# def valid_attributes
|
|
110
|
+
# {
|
|
111
|
+
# name: "Test <%= human_name %>",
|
|
112
|
+
# # Add other required attributes
|
|
113
|
+
# }
|
|
114
|
+
# end
|
|
115
|
+
|
|
116
|
+
# Define invalid attributes for validation tests
|
|
117
|
+
# def invalid_attributes
|
|
118
|
+
# {
|
|
119
|
+
# name: nil,
|
|
120
|
+
# # Add attributes that should fail validation
|
|
121
|
+
# }
|
|
122
|
+
# end
|
|
123
|
+
end
|
|
124
|
+
<% end -%>
|