better_service 1.1.0 → 2.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 +4 -4
- data/lib/better_service/cache_service.rb +148 -21
- data/lib/better_service/concerns/serviceable/repository_aware.rb +100 -0
- data/lib/better_service/concerns/serviceable/transactional.rb +38 -33
- data/lib/better_service/configuration.rb +35 -0
- data/lib/better_service/repository/base_repository.rb +257 -0
- data/lib/better_service/services/action_service.rb +6 -6
- data/lib/better_service/services/base.rb +97 -0
- data/lib/better_service/services/create_service.rb +5 -5
- data/lib/better_service/services/destroy_service.rb +5 -5
- data/lib/better_service/services/index_service.rb +2 -2
- data/lib/better_service/services/show_service.rb +2 -2
- data/lib/better_service/services/update_service.rb +5 -5
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/branch.rb +1 -1
- data/lib/better_service.rb +2 -0
- data/lib/generators/better_service/locale_generator.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ce89074c072a76a05a669dc079d3d4fbf1ba99a7f758f4ae45c9640b335c4c4
|
|
4
|
+
data.tar.gz: 34bcc32228b8c358f21f4713e783ef3f17e7e38047ab0116abad5a6e0d2a1aec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1479b3e94a12463614adb2022aa8a8d725939224715c5f5ae876be036f96da3c3a63171d6ca1cda4dc10ac3c6e97f98a615a45b952a9441066f24f064161521d
|
|
7
|
+
data.tar.gz: 2a985c1abfdbb8dc352fdc34351439f35bf0f09ed8784593e04e113a3bd2a38c1d172a3fb1f69836ab24cfd25881b59b52a5c54e5a4a3a6c8b8eacb1fcdb31f8
|
|
@@ -7,6 +7,9 @@ module BetterService
|
|
|
7
7
|
# the Cacheable concern. It provides methods to invalidate cache keys
|
|
8
8
|
# for specific users, contexts, or globally.
|
|
9
9
|
#
|
|
10
|
+
# Supports cascading invalidation through INVALIDATION_MAP - when a context
|
|
11
|
+
# is invalidated, all related contexts are also invalidated automatically.
|
|
12
|
+
#
|
|
10
13
|
# @example Invalidate cache for a specific context and user
|
|
11
14
|
# BetterService::CacheService.invalidate_for_context(current_user, "products")
|
|
12
15
|
#
|
|
@@ -15,61 +18,169 @@ module BetterService
|
|
|
15
18
|
#
|
|
16
19
|
# @example Invalidate all cache for a user
|
|
17
20
|
# BetterService::CacheService.invalidate_for_user(current_user)
|
|
21
|
+
#
|
|
22
|
+
# @example Configure invalidation map
|
|
23
|
+
# BetterService::CacheService.configure_invalidation_map(
|
|
24
|
+
# 'products' => %w[products inventory reports],
|
|
25
|
+
# 'orders' => %w[orders products reports]
|
|
26
|
+
# )
|
|
18
27
|
class CacheService
|
|
28
|
+
# Default invalidation map - can be customized via configure_invalidation_map
|
|
29
|
+
# Maps primary context to array of contexts that should be invalidated together
|
|
30
|
+
#
|
|
31
|
+
# @return [Hash<String, Array<String>>] Context invalidation mappings
|
|
32
|
+
@invalidation_map = {}
|
|
33
|
+
|
|
19
34
|
class << self
|
|
35
|
+
# Get the current invalidation map
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash<String, Array<String>>] Current invalidation mappings
|
|
38
|
+
attr_reader :invalidation_map
|
|
39
|
+
|
|
40
|
+
# Configure the invalidation map for cascading cache invalidation
|
|
41
|
+
#
|
|
42
|
+
# The invalidation map defines which cache contexts should be invalidated
|
|
43
|
+
# together. When a primary context is invalidated, all related contexts
|
|
44
|
+
# in the map are also invalidated.
|
|
45
|
+
#
|
|
46
|
+
# @param map [Hash<String, Array<String>>] Invalidation mappings
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
# @example Configure invalidation relationships
|
|
50
|
+
# BetterService::CacheService.configure_invalidation_map(
|
|
51
|
+
# 'products' => %w[products inventory reports],
|
|
52
|
+
# 'orders' => %w[orders products reports],
|
|
53
|
+
# 'users' => %w[users orders reports],
|
|
54
|
+
# 'categories' => %w[categories products reports],
|
|
55
|
+
# 'inventory' => %w[inventory products reports]
|
|
56
|
+
# )
|
|
57
|
+
def configure_invalidation_map(map)
|
|
58
|
+
@invalidation_map = map.transform_keys(&:to_s).transform_values do |contexts|
|
|
59
|
+
Array(contexts).map(&:to_s)
|
|
60
|
+
end.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Add entries to the invalidation map without replacing existing ones
|
|
64
|
+
#
|
|
65
|
+
# @param entries [Hash<String, Array<String>>] New invalidation mappings to add
|
|
66
|
+
# @return [void]
|
|
67
|
+
#
|
|
68
|
+
# @example Add new context invalidation rules
|
|
69
|
+
# BetterService::CacheService.add_invalidation_rules(
|
|
70
|
+
# 'payments' => %w[payments statistics invoices]
|
|
71
|
+
# )
|
|
72
|
+
def add_invalidation_rules(entries)
|
|
73
|
+
new_entries = entries.transform_keys(&:to_s).transform_values do |contexts|
|
|
74
|
+
Array(contexts).map(&:to_s)
|
|
75
|
+
end
|
|
76
|
+
@invalidation_map = (@invalidation_map || {}).merge(new_entries).freeze
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get all contexts that should be invalidated for a given context
|
|
80
|
+
#
|
|
81
|
+
# If the context exists in the invalidation map, returns all mapped contexts.
|
|
82
|
+
# Otherwise, returns an array containing just the original context.
|
|
83
|
+
#
|
|
84
|
+
# @param context [String, Symbol] The primary context
|
|
85
|
+
# @return [Array<String>] All contexts to invalidate
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# contexts_for('products') # => ['products', 'inventory', 'reports']
|
|
89
|
+
# contexts_for('unknown') # => ['unknown']
|
|
90
|
+
def contexts_to_invalidate(context)
|
|
91
|
+
context_str = context.to_s
|
|
92
|
+
(@invalidation_map || {})[context_str] || [context_str]
|
|
93
|
+
end
|
|
94
|
+
|
|
20
95
|
# Invalidate cache for a specific context and user
|
|
21
96
|
#
|
|
22
97
|
# Deletes all cache keys that match the pattern for the given user and context.
|
|
98
|
+
# Uses cascading invalidation - if the context exists in the invalidation map,
|
|
99
|
+
# all related contexts will also be invalidated.
|
|
100
|
+
#
|
|
23
101
|
# This is useful when data changes that affects a specific user's cached results.
|
|
24
102
|
#
|
|
25
103
|
# @param user [Object] The user whose cache should be invalidated
|
|
26
104
|
# @param context [String] The context name (e.g., "products", "sidebar")
|
|
27
105
|
# @param async [Boolean] Whether to perform invalidation asynchronously
|
|
106
|
+
# @param cascade [Boolean] Whether to use cascading invalidation (default: true)
|
|
28
107
|
# @return [Integer] Number of keys deleted (if supported by cache store)
|
|
29
108
|
#
|
|
30
|
-
# @example
|
|
109
|
+
# @example Basic invalidation
|
|
31
110
|
# # After creating a product, invalidate products cache for user
|
|
32
111
|
# BetterService::CacheService.invalidate_for_context(user, "products")
|
|
33
|
-
|
|
112
|
+
#
|
|
113
|
+
# @example With cascading (if map configured for orders -> [orders, products, reports])
|
|
114
|
+
# BetterService::CacheService.invalidate_for_context(user, "orders")
|
|
115
|
+
# # Invalidates: orders, products, reports caches
|
|
116
|
+
#
|
|
117
|
+
# @example Without cascading
|
|
118
|
+
# BetterService::CacheService.invalidate_for_context(user, "orders", cascade: false)
|
|
119
|
+
# # Invalidates: only orders cache
|
|
120
|
+
def invalidate_for_context(user, context, async: false, cascade: true)
|
|
34
121
|
return 0 unless user && context && !context.to_s.strip.empty?
|
|
35
122
|
|
|
36
|
-
|
|
123
|
+
# Get all contexts to invalidate (cascading or single)
|
|
124
|
+
contexts = cascade ? contexts_to_invalidate(context) : [context.to_s]
|
|
125
|
+
total_deleted = 0
|
|
37
126
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
127
|
+
contexts.each do |ctx|
|
|
128
|
+
pattern = build_user_context_pattern(user, ctx)
|
|
129
|
+
|
|
130
|
+
if async
|
|
131
|
+
invalidate_async(pattern)
|
|
132
|
+
else
|
|
133
|
+
result = delete_matched(pattern)
|
|
134
|
+
count = result.is_a?(Array) ? result.size : (result || 0)
|
|
135
|
+
total_deleted += count
|
|
136
|
+
end
|
|
45
137
|
end
|
|
138
|
+
|
|
139
|
+
log_cascading_invalidation(context, contexts) if cascade && contexts.size > 1
|
|
140
|
+
total_deleted
|
|
46
141
|
end
|
|
47
142
|
|
|
48
143
|
# Invalidate cache globally for a context
|
|
49
144
|
#
|
|
50
145
|
# Deletes all cache keys for the given context across all users.
|
|
146
|
+
# Uses cascading invalidation - if the context exists in the invalidation map,
|
|
147
|
+
# all related contexts will also be invalidated.
|
|
148
|
+
#
|
|
51
149
|
# This is useful when data changes that affects everyone (e.g., global settings).
|
|
52
150
|
#
|
|
53
151
|
# @param context [String] The context name
|
|
54
152
|
# @param async [Boolean] Whether to perform invalidation asynchronously
|
|
153
|
+
# @param cascade [Boolean] Whether to use cascading invalidation (default: true)
|
|
55
154
|
# @return [Integer] Number of keys deleted (if supported by cache store)
|
|
56
155
|
#
|
|
57
|
-
# @example
|
|
156
|
+
# @example Basic global invalidation
|
|
58
157
|
# # After updating global sidebar settings
|
|
59
158
|
# BetterService::CacheService.invalidate_global("sidebar")
|
|
60
|
-
|
|
159
|
+
#
|
|
160
|
+
# @example With cascading
|
|
161
|
+
# BetterService::CacheService.invalidate_global("orders")
|
|
162
|
+
# # Invalidates: orders, products, reports caches globally
|
|
163
|
+
def invalidate_global(context, async: false, cascade: true)
|
|
61
164
|
return 0 unless context && !context.to_s.strip.empty?
|
|
62
165
|
|
|
63
|
-
|
|
166
|
+
# Get all contexts to invalidate (cascading or single)
|
|
167
|
+
contexts = cascade ? contexts_to_invalidate(context) : [context.to_s]
|
|
168
|
+
total_deleted = 0
|
|
64
169
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
170
|
+
contexts.each do |ctx|
|
|
171
|
+
pattern = build_global_context_pattern(ctx)
|
|
172
|
+
|
|
173
|
+
if async
|
|
174
|
+
invalidate_async(pattern)
|
|
175
|
+
else
|
|
176
|
+
result = delete_matched(pattern)
|
|
177
|
+
count = result.is_a?(Array) ? result.size : (result || 0)
|
|
178
|
+
total_deleted += count
|
|
179
|
+
end
|
|
72
180
|
end
|
|
181
|
+
|
|
182
|
+
log_cascading_invalidation(context, contexts, global: true) if cascade && contexts.size > 1
|
|
183
|
+
total_deleted
|
|
73
184
|
end
|
|
74
185
|
|
|
75
186
|
# Invalidate all cache for a specific user
|
|
@@ -175,7 +286,9 @@ module BetterService
|
|
|
175
286
|
{
|
|
176
287
|
cache_store: Rails.cache.class.name,
|
|
177
288
|
supports_pattern_deletion: supports_delete_matched?,
|
|
178
|
-
supports_async: defined?(ActiveJob) ? true : false
|
|
289
|
+
supports_async: defined?(ActiveJob) ? true : false,
|
|
290
|
+
invalidation_map_configured: (@invalidation_map || {}).any?,
|
|
291
|
+
invalidation_map_contexts: (@invalidation_map || {}).keys
|
|
179
292
|
}
|
|
180
293
|
end
|
|
181
294
|
|
|
@@ -288,6 +401,20 @@ module BetterService
|
|
|
288
401
|
|
|
289
402
|
Rails.logger.warn "[BetterService::CacheService] #{message}"
|
|
290
403
|
end
|
|
404
|
+
|
|
405
|
+
# Log cascading invalidation
|
|
406
|
+
#
|
|
407
|
+
# @param primary_context [String] The primary context that triggered invalidation
|
|
408
|
+
# @param all_contexts [Array<String>] All contexts that were invalidated
|
|
409
|
+
# @param global [Boolean] Whether this was a global invalidation
|
|
410
|
+
# @return [void]
|
|
411
|
+
def log_cascading_invalidation(primary_context, all_contexts, global: false)
|
|
412
|
+
return unless defined?(Rails) && Rails.logger
|
|
413
|
+
|
|
414
|
+
scope = global ? "globally" : "for user"
|
|
415
|
+
Rails.logger.info "[BetterService::CacheService] Cascading invalidation #{scope}: " \
|
|
416
|
+
"'#{primary_context}' -> [#{all_contexts.join(', ')}]"
|
|
417
|
+
end
|
|
291
418
|
end
|
|
292
419
|
|
|
293
420
|
# ActiveJob for async cache invalidation
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Concerns
|
|
5
|
+
module Serviceable
|
|
6
|
+
# RepositoryAware - DSL for declaring repository dependencies in services
|
|
7
|
+
#
|
|
8
|
+
# This concern provides a clean way to declare repository dependencies
|
|
9
|
+
# in service classes, promoting the repository pattern and enforcing
|
|
10
|
+
# separation between business logic and data access.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# class Products::CreateService < BetterService::Services::CreateService
|
|
14
|
+
# include BetterService::Concerns::Serviceable::RepositoryAware
|
|
15
|
+
#
|
|
16
|
+
# repository :product
|
|
17
|
+
#
|
|
18
|
+
# process_with do |data|
|
|
19
|
+
# { resource: product_repository.create!(params) }
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example With custom class name
|
|
24
|
+
# class Bookings::AcceptService < BetterService::Services::ActionService
|
|
25
|
+
# include BetterService::Concerns::Serviceable::RepositoryAware
|
|
26
|
+
#
|
|
27
|
+
# repository :booking, class_name: "Bookings::BookingRepository"
|
|
28
|
+
# repository :user, class_name: "Users::UserRepository", as: :user_repo
|
|
29
|
+
#
|
|
30
|
+
# search_with do
|
|
31
|
+
# { booking: booking_repository.search({ id_eq: params[:id] }, limit: 1) }
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @example Multiple repositories shorthand
|
|
36
|
+
# class Dashboard::IndexService < BetterService::Services::IndexService
|
|
37
|
+
# include BetterService::Concerns::Serviceable::RepositoryAware
|
|
38
|
+
#
|
|
39
|
+
# repositories :user, :booking, :payment
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
module RepositoryAware
|
|
43
|
+
extend ActiveSupport::Concern
|
|
44
|
+
|
|
45
|
+
class_methods do
|
|
46
|
+
# Declare a repository dependency
|
|
47
|
+
#
|
|
48
|
+
# Creates a memoized private accessor method for the repository.
|
|
49
|
+
#
|
|
50
|
+
# @param name [Symbol] Base name for the repository
|
|
51
|
+
# @param class_name [String, nil] Full class name of the repository
|
|
52
|
+
# If nil, derives from name: :product -> "ProductRepository"
|
|
53
|
+
# @param as [Symbol, nil] Custom accessor name
|
|
54
|
+
# If nil, uses "#{name}_repository"
|
|
55
|
+
# @return [void]
|
|
56
|
+
#
|
|
57
|
+
# @example Standard naming
|
|
58
|
+
# repository :product # -> product_repository -> ProductRepository
|
|
59
|
+
#
|
|
60
|
+
# @example Custom class
|
|
61
|
+
# repository :booking, class_name: "Bookings::BookingRepository"
|
|
62
|
+
#
|
|
63
|
+
# @example Custom accessor
|
|
64
|
+
# repository :user, as: :users # -> users -> UserRepository
|
|
65
|
+
def repository(name, class_name: nil, as: nil)
|
|
66
|
+
accessor_name = as || "#{name}_repository"
|
|
67
|
+
repo_class_name = class_name || "#{name.to_s.camelize}Repository"
|
|
68
|
+
|
|
69
|
+
define_method(accessor_name) do
|
|
70
|
+
ivar = "@#{accessor_name}"
|
|
71
|
+
instance_variable_get(ivar) || begin
|
|
72
|
+
klass = repo_class_name.constantize
|
|
73
|
+
instance_variable_set(ivar, klass.new)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private accessor_name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Declare multiple repository dependencies
|
|
81
|
+
#
|
|
82
|
+
# Shorthand for declaring multiple repositories with standard naming.
|
|
83
|
+
#
|
|
84
|
+
# @param names [Array<Symbol>] Repository names
|
|
85
|
+
# @return [void]
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# repositories :user, :booking, :payment
|
|
89
|
+
# # Equivalent to:
|
|
90
|
+
# # repository :user
|
|
91
|
+
# # repository :booking
|
|
92
|
+
# # repository :payment
|
|
93
|
+
def repositories(*names)
|
|
94
|
+
names.each { |name| repository(name) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -3,49 +3,54 @@
|
|
|
3
3
|
module BetterService
|
|
4
4
|
module Concerns
|
|
5
5
|
module Serviceable
|
|
6
|
-
|
|
7
|
-
extend ActiveSupport::Concern
|
|
8
|
-
|
|
9
|
-
included do
|
|
10
|
-
class_attribute :_with_transaction, default: false
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Hook for prepend (same as included but triggered by prepend)
|
|
14
|
-
def self.prepended(base)
|
|
15
|
-
base.class_attribute :_with_transaction, default: false
|
|
16
|
-
base.extend(ClassMethods)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
module ClassMethods
|
|
20
|
-
# Enable or disable database transactions for this service
|
|
6
|
+
# Provides transaction wrapping for service execution
|
|
21
7
|
#
|
|
22
|
-
#
|
|
8
|
+
# This concern is PREPENDED (not included) to Services::Base to wrap
|
|
9
|
+
# the process method in a database transaction when enabled.
|
|
23
10
|
#
|
|
24
|
-
# @example Enable transactions
|
|
11
|
+
# @example Enable transactions in a service
|
|
25
12
|
# class Booking::CreateService < BetterService::CreateService
|
|
26
13
|
# with_transaction true
|
|
27
14
|
# end
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
module Transactional
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
# Hook for prepend - sets up class attributes and class methods
|
|
19
|
+
def self.prepended(base)
|
|
20
|
+
base.class_attribute :_with_transaction, default: false
|
|
21
|
+
base.extend(ClassMethods)
|
|
35
22
|
end
|
|
36
|
-
end
|
|
37
23
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Enable or disable database transactions for this service
|
|
26
|
+
#
|
|
27
|
+
# @param value [Boolean] whether to wrap process in a transaction
|
|
28
|
+
#
|
|
29
|
+
# @example Enable transactions
|
|
30
|
+
# class Booking::CreateService < BetterService::CreateService
|
|
31
|
+
# with_transaction true
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example Disable transactions
|
|
35
|
+
# class Booking::ImportService < BetterService::CreateService
|
|
36
|
+
# with_transaction false # Disable inherited transaction
|
|
37
|
+
# end
|
|
38
|
+
def with_transaction(value)
|
|
39
|
+
self._with_transaction = value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Override process to wrap in transaction if enabled
|
|
44
|
+
def process(data)
|
|
45
|
+
return super(data) unless self.class._with_transaction
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
result = nil
|
|
48
|
+
ActiveRecord::Base.transaction do
|
|
49
|
+
result = super(data)
|
|
50
|
+
end
|
|
51
|
+
result
|
|
45
52
|
end
|
|
46
|
-
result
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
|
-
end
|
|
50
55
|
end
|
|
51
56
|
end
|
|
@@ -11,6 +11,13 @@ module BetterService
|
|
|
11
11
|
# config.instrumentation_enabled = true
|
|
12
12
|
# config.instrumentation_include_args = false
|
|
13
13
|
# config.instrumentation_excluded_services = ["HealthCheckService"]
|
|
14
|
+
#
|
|
15
|
+
# # Cache invalidation map for cascading cache invalidation
|
|
16
|
+
# config.cache_invalidation_map = {
|
|
17
|
+
# 'products' => %w[products inventory reports],
|
|
18
|
+
# 'orders' => %w[orders products reports],
|
|
19
|
+
# 'users' => %w[users orders reports]
|
|
20
|
+
# }
|
|
14
21
|
# end
|
|
15
22
|
class Configuration
|
|
16
23
|
# Enable/disable instrumentation globally
|
|
@@ -65,6 +72,31 @@ module BetterService
|
|
|
65
72
|
# @return [Boolean] Default: false
|
|
66
73
|
attr_accessor :stats_subscriber_enabled
|
|
67
74
|
|
|
75
|
+
# Cache invalidation map for cascading cache invalidation
|
|
76
|
+
#
|
|
77
|
+
# When a context is invalidated, all related contexts in the map
|
|
78
|
+
# are also invalidated automatically.
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash<String, Array<String>>] Default: {}
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# config.cache_invalidation_map = {
|
|
84
|
+
# 'products' => %w[products inventory reports],
|
|
85
|
+
# 'orders' => %w[orders products reports]
|
|
86
|
+
# }
|
|
87
|
+
attr_reader :cache_invalidation_map
|
|
88
|
+
|
|
89
|
+
# Set the cache invalidation map
|
|
90
|
+
#
|
|
91
|
+
# Automatically configures CacheService with the provided map.
|
|
92
|
+
#
|
|
93
|
+
# @param map [Hash<String, Array<String>>] Invalidation mappings
|
|
94
|
+
# @return [void]
|
|
95
|
+
def cache_invalidation_map=(map)
|
|
96
|
+
@cache_invalidation_map = map
|
|
97
|
+
CacheService.configure_invalidation_map(map) if map
|
|
98
|
+
end
|
|
99
|
+
|
|
68
100
|
def initialize
|
|
69
101
|
# Instrumentation defaults
|
|
70
102
|
@instrumentation_enabled = true
|
|
@@ -76,6 +108,9 @@ module BetterService
|
|
|
76
108
|
@log_subscriber_enabled = false
|
|
77
109
|
@log_subscriber_level = :info
|
|
78
110
|
@stats_subscriber_enabled = false
|
|
111
|
+
|
|
112
|
+
# Cache defaults
|
|
113
|
+
@cache_invalidation_map = {}
|
|
79
114
|
end
|
|
80
115
|
end
|
|
81
116
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Repository
|
|
5
|
+
# BaseRepository - Generic repository pattern for data access
|
|
6
|
+
#
|
|
7
|
+
# Provides a clean abstraction layer between services and ActiveRecord models.
|
|
8
|
+
# Repositories handle all database queries using predicates-based search,
|
|
9
|
+
# enforcing separation of concerns and enabling easier testing.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# class ProductRepository < BetterService::Repository::BaseRepository
|
|
13
|
+
# def initialize(model_class = Product)
|
|
14
|
+
# super
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# repo = ProductRepository.new
|
|
19
|
+
# products = repo.search({ status_eq: 'active' }, includes: [:category])
|
|
20
|
+
#
|
|
21
|
+
# @example With predicates
|
|
22
|
+
# repo.search({
|
|
23
|
+
# user_id_eq: user.id,
|
|
24
|
+
# status_in: ['pending', 'confirmed'],
|
|
25
|
+
# created_at_gteq: 1.week.ago
|
|
26
|
+
# }, order: 'created_at DESC', per_page: 20)
|
|
27
|
+
#
|
|
28
|
+
class BaseRepository
|
|
29
|
+
attr_reader :model
|
|
30
|
+
|
|
31
|
+
# Initialize repository with a model class
|
|
32
|
+
#
|
|
33
|
+
# @param model_class [Class, nil] ActiveRecord model class
|
|
34
|
+
# If nil, derives class name from repository name
|
|
35
|
+
def initialize(model_class = nil)
|
|
36
|
+
@model = model_class || derive_model_class
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Search records using predicates
|
|
40
|
+
#
|
|
41
|
+
# Supports flexible querying through predicates hash that gets
|
|
42
|
+
# translated to ActiveRecord scopes via model's Searchable concern.
|
|
43
|
+
#
|
|
44
|
+
# @param predicates [Hash] Search predicates (e.g., { status_eq: 'active' })
|
|
45
|
+
# @param page [Integer] Page number for pagination (default: 1)
|
|
46
|
+
# @param per_page [Integer] Records per page (default: 20)
|
|
47
|
+
# @param includes [Array] Associations to eager load
|
|
48
|
+
# @param joins [Array] Associations to join
|
|
49
|
+
# @param order [String, Hash, nil] Order clause
|
|
50
|
+
# @param order_scope [Hash, nil] Named scope for ordering { field:, direction: }
|
|
51
|
+
# @param limit [Integer, Symbol, nil] Limit results
|
|
52
|
+
# - 1: returns single record (first)
|
|
53
|
+
# - Integer > 1: limit to N records
|
|
54
|
+
# - nil: no limit (returns all)
|
|
55
|
+
# - :default: apply pagination
|
|
56
|
+
# @return [ActiveRecord::Relation, Object, nil] Query result
|
|
57
|
+
#
|
|
58
|
+
# @example Basic search
|
|
59
|
+
# search({ status_eq: 'active' })
|
|
60
|
+
#
|
|
61
|
+
# @example With pagination
|
|
62
|
+
# search({ user_id_eq: 1 }, page: 2, per_page: 25)
|
|
63
|
+
#
|
|
64
|
+
# @example Single record
|
|
65
|
+
# search({ id_eq: 123 }, limit: 1)
|
|
66
|
+
#
|
|
67
|
+
# @example With eager loading
|
|
68
|
+
# search({}, includes: [:user, :comments], order: 'created_at DESC')
|
|
69
|
+
def search(predicates = {}, page: 1, per_page: 20, includes: [],
|
|
70
|
+
joins: [], order: nil, order_scope: nil, limit: :default)
|
|
71
|
+
cleaned_predicates = (predicates || {}).compact
|
|
72
|
+
|
|
73
|
+
scope = build_base_scope(cleaned_predicates)
|
|
74
|
+
scope = apply_joins(scope, joins)
|
|
75
|
+
scope = apply_includes(scope, includes)
|
|
76
|
+
scope = apply_ordering(scope, order, order_scope)
|
|
77
|
+
apply_limit_or_pagination(scope, limit, page, per_page)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Delegate basic ActiveRecord methods to model
|
|
81
|
+
delegate :find, :find_by, :where, :all, :count, :exists?, to: :model
|
|
82
|
+
|
|
83
|
+
# Build a new unsaved record
|
|
84
|
+
#
|
|
85
|
+
# @param attributes [Hash] Attributes for the new record
|
|
86
|
+
# @return [ActiveRecord::Base] Unsaved model instance
|
|
87
|
+
def build(attributes = {})
|
|
88
|
+
model.new(attributes)
|
|
89
|
+
end
|
|
90
|
+
alias new build
|
|
91
|
+
|
|
92
|
+
# Create a new record (may return invalid record)
|
|
93
|
+
#
|
|
94
|
+
# @param attributes [Hash] Attributes for the new record
|
|
95
|
+
# @return [ActiveRecord::Base] Created model instance
|
|
96
|
+
def create(attributes = {})
|
|
97
|
+
model.create(attributes)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Create a new record (raises on validation failure)
|
|
101
|
+
#
|
|
102
|
+
# @param attributes [Hash] Attributes for the new record
|
|
103
|
+
# @return [ActiveRecord::Base] Created model instance
|
|
104
|
+
# @raise [ActiveRecord::RecordInvalid] if validation fails
|
|
105
|
+
def create!(attributes = {})
|
|
106
|
+
model.create!(attributes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Update an existing record
|
|
110
|
+
#
|
|
111
|
+
# @param record_or_id [ActiveRecord::Base, Integer, String] Record or ID
|
|
112
|
+
# @param attributes [Hash] Attributes to update
|
|
113
|
+
# @return [ActiveRecord::Base] Updated model instance
|
|
114
|
+
# @raise [ActiveRecord::RecordInvalid] if validation fails
|
|
115
|
+
def update(record_or_id, attributes)
|
|
116
|
+
record = resolve_record(record_or_id)
|
|
117
|
+
record.update!(attributes)
|
|
118
|
+
record
|
|
119
|
+
end
|
|
120
|
+
alias update! update
|
|
121
|
+
|
|
122
|
+
# Destroy a record
|
|
123
|
+
#
|
|
124
|
+
# @param record_or_id [ActiveRecord::Base, Integer, String] Record or ID
|
|
125
|
+
# @return [ActiveRecord::Base] Destroyed model instance
|
|
126
|
+
def destroy(record_or_id)
|
|
127
|
+
record = resolve_record(record_or_id)
|
|
128
|
+
record.destroy!
|
|
129
|
+
record
|
|
130
|
+
end
|
|
131
|
+
alias destroy! destroy
|
|
132
|
+
|
|
133
|
+
# Delete a record without callbacks
|
|
134
|
+
#
|
|
135
|
+
# @param record_or_id [ActiveRecord::Base, Integer, String] Record or ID
|
|
136
|
+
# @return [Integer] Number of deleted records
|
|
137
|
+
def delete(record_or_id)
|
|
138
|
+
id = record_or_id.respond_to?(:id) ? record_or_id.id : record_or_id
|
|
139
|
+
model.where(id: id).delete_all
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# Resolve a record from ID or return the record itself
|
|
145
|
+
#
|
|
146
|
+
# @param record_or_id [ActiveRecord::Base, Integer, String] Record or ID
|
|
147
|
+
# @return [ActiveRecord::Base] The resolved record
|
|
148
|
+
def resolve_record(record_or_id)
|
|
149
|
+
if record_or_id.is_a?(model)
|
|
150
|
+
record_or_id
|
|
151
|
+
else
|
|
152
|
+
find(record_or_id)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Derive model class from repository name
|
|
157
|
+
#
|
|
158
|
+
# ProductRepository -> Product
|
|
159
|
+
# Bookings::BookingRepository -> Bookings::Booking
|
|
160
|
+
#
|
|
161
|
+
# @return [Class] The derived model class
|
|
162
|
+
# @raise [BetterService::Errors::Configuration::ConfigurationError]
|
|
163
|
+
def derive_model_class
|
|
164
|
+
class_name = self.class.name.gsub(/Repository$/, "")
|
|
165
|
+
class_name.constantize
|
|
166
|
+
rescue NameError
|
|
167
|
+
raise Errors::Configuration::ConfigurationError,
|
|
168
|
+
"Could not derive model class from #{self.class.name}. " \
|
|
169
|
+
"Pass model_class explicitly to initialize."
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Build base scope from predicates
|
|
173
|
+
#
|
|
174
|
+
# @param predicates [Hash] Search predicates
|
|
175
|
+
# @return [ActiveRecord::Relation] Base query scope
|
|
176
|
+
def build_base_scope(predicates)
|
|
177
|
+
if model.respond_to?(:search) && predicates.present?
|
|
178
|
+
model.search(predicates)
|
|
179
|
+
else
|
|
180
|
+
model.all
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Apply joins to scope
|
|
185
|
+
#
|
|
186
|
+
# @param scope [ActiveRecord::Relation] Current scope
|
|
187
|
+
# @param joins [Array] Associations to join
|
|
188
|
+
# @return [ActiveRecord::Relation] Scope with joins
|
|
189
|
+
def apply_joins(scope, joins)
|
|
190
|
+
return scope if joins.blank?
|
|
191
|
+
|
|
192
|
+
scope.joins(*joins)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Apply includes to scope
|
|
196
|
+
#
|
|
197
|
+
# @param scope [ActiveRecord::Relation] Current scope
|
|
198
|
+
# @param includes [Array] Associations to eager load
|
|
199
|
+
# @return [ActiveRecord::Relation] Scope with includes
|
|
200
|
+
def apply_includes(scope, includes)
|
|
201
|
+
return scope if includes.blank?
|
|
202
|
+
|
|
203
|
+
scope.includes(*includes)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Apply ordering to scope
|
|
207
|
+
#
|
|
208
|
+
# @param scope [ActiveRecord::Relation] Current scope
|
|
209
|
+
# @param order [String, Hash, nil] Order clause
|
|
210
|
+
# @param order_scope [Hash, nil] Named scope for ordering
|
|
211
|
+
# @return [ActiveRecord::Relation] Ordered scope
|
|
212
|
+
def apply_ordering(scope, order, order_scope)
|
|
213
|
+
if order_scope.present?
|
|
214
|
+
scope_name = "#{order_scope[:field]}_#{order_scope[:direction]}"
|
|
215
|
+
scope.respond_to?(scope_name) ? scope.send(scope_name) : scope
|
|
216
|
+
elsif order.present?
|
|
217
|
+
scope.order(order)
|
|
218
|
+
else
|
|
219
|
+
scope
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Apply limit or pagination to scope
|
|
224
|
+
#
|
|
225
|
+
# @param scope [ActiveRecord::Relation] Current scope
|
|
226
|
+
# @param limit [Integer, Symbol, nil] Limit specification
|
|
227
|
+
# @param page [Integer] Page number
|
|
228
|
+
# @param per_page [Integer] Records per page
|
|
229
|
+
# @return [ActiveRecord::Relation, Object, nil] Limited scope or record
|
|
230
|
+
def apply_limit_or_pagination(scope, limit, page, per_page)
|
|
231
|
+
case limit
|
|
232
|
+
when 1
|
|
233
|
+
scope.first
|
|
234
|
+
when Integer
|
|
235
|
+
scope.limit(limit)
|
|
236
|
+
when nil
|
|
237
|
+
scope
|
|
238
|
+
when :default
|
|
239
|
+
paginate(scope, page: page, per_page: per_page)
|
|
240
|
+
else
|
|
241
|
+
paginate(scope, page: page, per_page: per_page)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Apply pagination to scope
|
|
246
|
+
#
|
|
247
|
+
# @param scope [ActiveRecord::Relation] Current scope
|
|
248
|
+
# @param page [Integer] Page number
|
|
249
|
+
# @param per_page [Integer] Records per page
|
|
250
|
+
# @return [ActiveRecord::Relation] Paginated scope
|
|
251
|
+
def paginate(scope, page:, per_page:)
|
|
252
|
+
offset_value = ([page.to_i, 1].max - 1) * per_page.to_i
|
|
253
|
+
scope.offset(offset_value).limit(per_page)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -9,21 +9,21 @@ module BetterService
|
|
|
9
9
|
# Returns: { resource: {}, metadata: { action: :custom_action_name } }
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# class
|
|
13
|
-
# action_name :
|
|
12
|
+
# class Orders::ConfirmService < BetterService::Services::ActionService
|
|
13
|
+
# action_name :confirmed # Sets metadata action
|
|
14
14
|
#
|
|
15
15
|
# schema do
|
|
16
16
|
# required(:id).filled(:integer)
|
|
17
17
|
# end
|
|
18
18
|
#
|
|
19
19
|
# search_with do
|
|
20
|
-
# { resource: user.
|
|
20
|
+
# { resource: user.orders.find(params[:id]) }
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
23
|
# process_with do |data|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# { resource:
|
|
24
|
+
# order = data[:resource]
|
|
25
|
+
# order.update!(status: 'confirmed', confirmed_at: Time.current)
|
|
26
|
+
# { resource: order }
|
|
27
27
|
# end
|
|
28
28
|
# end
|
|
29
29
|
class ActionService < Services::Base
|
|
@@ -298,6 +298,103 @@ module BetterService
|
|
|
298
298
|
}
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
+
# Build a failure response hash
|
|
302
|
+
#
|
|
303
|
+
# @param error_message [String] Human-readable error message
|
|
304
|
+
# @param errors [Hash, Array] Validation errors in various formats
|
|
305
|
+
# @return [Hash] Standardized failure response
|
|
306
|
+
#
|
|
307
|
+
# @example Simple error
|
|
308
|
+
# failure_result("Record not found")
|
|
309
|
+
#
|
|
310
|
+
# @example With validation errors
|
|
311
|
+
# failure_result("Validation failed", { email: ["is invalid"] })
|
|
312
|
+
def failure_result(error_message, errors = {})
|
|
313
|
+
{
|
|
314
|
+
success: false,
|
|
315
|
+
error: error_message,
|
|
316
|
+
errors: format_errors_for_response(errors)
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Build a validation failure response with the failed resource
|
|
321
|
+
#
|
|
322
|
+
# Used when ActiveRecord validation fails and the form
|
|
323
|
+
# needs to be re-rendered with the invalid model.
|
|
324
|
+
#
|
|
325
|
+
# @param failed_resource [ActiveRecord::Base] Model with validation errors
|
|
326
|
+
# @return [Hash] Failure response with errors and failed_resource
|
|
327
|
+
#
|
|
328
|
+
# @example
|
|
329
|
+
# user = User.new(invalid_params)
|
|
330
|
+
# user.valid? # => false
|
|
331
|
+
# validation_failure_result(user)
|
|
332
|
+
def validation_failure_result(failed_resource)
|
|
333
|
+
{
|
|
334
|
+
success: false,
|
|
335
|
+
errors: format_model_validation_errors(failed_resource.errors),
|
|
336
|
+
failed_resource: failed_resource
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Check if data contains an error (for phase flow control)
|
|
341
|
+
#
|
|
342
|
+
# @param data [Hash] Data from previous phase
|
|
343
|
+
# @return [Boolean] true if data contains error
|
|
344
|
+
#
|
|
345
|
+
# @example
|
|
346
|
+
# def process(data)
|
|
347
|
+
# return data if error?(data) # Pass through errors
|
|
348
|
+
# # ... processing logic
|
|
349
|
+
# end
|
|
350
|
+
def error?(data)
|
|
351
|
+
data.is_a?(Hash) && (data[:error] || data[:success] == false)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Format errors into standard array format
|
|
355
|
+
#
|
|
356
|
+
# @param errors [Hash, Array] Errors in various formats
|
|
357
|
+
# @return [Array<Hash>] Standardized error array
|
|
358
|
+
def format_errors_for_response(errors)
|
|
359
|
+
case errors
|
|
360
|
+
when Hash
|
|
361
|
+
errors.flat_map do |key, messages|
|
|
362
|
+
Array(messages).map { |msg| { key: key.to_s, message: msg } }
|
|
363
|
+
end
|
|
364
|
+
when Array
|
|
365
|
+
errors.map do |err|
|
|
366
|
+
if err.is_a?(Hash) && err[:key] && err[:message]
|
|
367
|
+
err
|
|
368
|
+
elsif err.is_a?(String)
|
|
369
|
+
{ key: "base", message: err }
|
|
370
|
+
else
|
|
371
|
+
{ key: "base", message: err.to_s }
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
else
|
|
375
|
+
[]
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Format ActiveRecord/ActiveModel errors into standard format
|
|
380
|
+
#
|
|
381
|
+
# Used specifically for ActiveModel::Errors objects (from Rails models).
|
|
382
|
+
# For Dry::Schema validation errors, the Validatable concern has its own
|
|
383
|
+
# format_validation_errors method.
|
|
384
|
+
#
|
|
385
|
+
# @param errors [ActiveModel::Errors] ActiveRecord errors object
|
|
386
|
+
# @return [Array<Hash>] Standardized error array
|
|
387
|
+
def format_model_validation_errors(errors)
|
|
388
|
+
return [] if errors.blank?
|
|
389
|
+
|
|
390
|
+
errors.map do |error|
|
|
391
|
+
{
|
|
392
|
+
key: error.attribute.to_s,
|
|
393
|
+
message: error.message
|
|
394
|
+
}
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
301
398
|
# Prepend Instrumentation at the end, after call method is defined
|
|
302
399
|
# This wraps the entire call method (including cache logic from Cacheable)
|
|
303
400
|
prepend Concerns::Instrumentation
|
|
@@ -9,10 +9,10 @@ module BetterService
|
|
|
9
9
|
# Returns: { resource: {}, metadata: { action: :created } }
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# class
|
|
12
|
+
# class Orders::CreateService < BetterService::Services::CreateService
|
|
13
13
|
# schema do
|
|
14
14
|
# required(:title).filled(:string)
|
|
15
|
-
# required(:
|
|
15
|
+
# required(:total).filled(:decimal)
|
|
16
16
|
# end
|
|
17
17
|
#
|
|
18
18
|
# search_with do
|
|
@@ -20,11 +20,11 @@ module BetterService
|
|
|
20
20
|
# end
|
|
21
21
|
#
|
|
22
22
|
# process_with do |data|
|
|
23
|
-
#
|
|
23
|
+
# order = user.orders.create!(
|
|
24
24
|
# title: params[:title],
|
|
25
|
-
#
|
|
25
|
+
# total: params[:total]
|
|
26
26
|
# )
|
|
27
|
-
# { resource:
|
|
27
|
+
# { resource: order }
|
|
28
28
|
# end
|
|
29
29
|
# end
|
|
30
30
|
class CreateService < Services::Base
|
|
@@ -9,19 +9,19 @@ module BetterService
|
|
|
9
9
|
# Returns: { resource: {}, metadata: { action: :deleted } }
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# class
|
|
12
|
+
# class Orders::DestroyService < BetterService::Services::DestroyService
|
|
13
13
|
# schema do
|
|
14
14
|
# required(:id).filled(:integer)
|
|
15
15
|
# end
|
|
16
16
|
#
|
|
17
17
|
# search_with do
|
|
18
|
-
# { resource: user.
|
|
18
|
+
# { resource: user.orders.find(params[:id]) }
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
21
|
# process_with do |data|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# { resource:
|
|
22
|
+
# order = data[:resource]
|
|
23
|
+
# order.destroy!
|
|
24
|
+
# { resource: order }
|
|
25
25
|
# end
|
|
26
26
|
# end
|
|
27
27
|
class DestroyService < Services::Base
|
|
@@ -9,9 +9,9 @@ module BetterService
|
|
|
9
9
|
# Returns: { items: [], metadata: { action: :index, stats: {}, pagination: {} } }
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# class
|
|
12
|
+
# class Orders::IndexService < BetterService::Services::IndexService
|
|
13
13
|
# search_with do
|
|
14
|
-
# { items: user.
|
|
14
|
+
# { items: user.orders.to_a }
|
|
15
15
|
# end
|
|
16
16
|
#
|
|
17
17
|
# process_with do |data|
|
|
@@ -9,9 +9,9 @@ module BetterService
|
|
|
9
9
|
# Returns: { resource: {}, metadata: { action: :show } }
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# class
|
|
12
|
+
# class Orders::ShowService < BetterService::Services::ShowService
|
|
13
13
|
# search_with do
|
|
14
|
-
# { resource: user.
|
|
14
|
+
# { resource: user.orders.find(params[:id]) }
|
|
15
15
|
# end
|
|
16
16
|
# end
|
|
17
17
|
class ShowService < Services::Base
|
|
@@ -9,20 +9,20 @@ module BetterService
|
|
|
9
9
|
# Returns: { resource: {}, metadata: { action: :updated } }
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# class
|
|
12
|
+
# class Orders::UpdateService < BetterService::Services::UpdateService
|
|
13
13
|
# schema do
|
|
14
14
|
# required(:id).filled(:integer)
|
|
15
15
|
# optional(:title).filled(:string)
|
|
16
16
|
# end
|
|
17
17
|
#
|
|
18
18
|
# search_with do
|
|
19
|
-
# { resource: user.
|
|
19
|
+
# { resource: user.orders.find(params[:id]) }
|
|
20
20
|
# end
|
|
21
21
|
#
|
|
22
22
|
# process_with do |data|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# { resource:
|
|
23
|
+
# order = data[:resource]
|
|
24
|
+
# order.update!(params.except(:id))
|
|
25
|
+
# { resource: order }
|
|
26
26
|
# end
|
|
27
27
|
# end
|
|
28
28
|
class UpdateService < Services::Base
|
|
@@ -97,7 +97,7 @@ module BetterService
|
|
|
97
97
|
if result[:success] == false && !result[:optional_failure]
|
|
98
98
|
raise Errors::Workflowable::Runtime::StepExecutionError.new(
|
|
99
99
|
"Step #{step_or_branch_group.name} failed in branch",
|
|
100
|
-
code: ErrorCodes::
|
|
100
|
+
code: ErrorCodes::STEP_FAILED,
|
|
101
101
|
context: {
|
|
102
102
|
step: step_or_branch_group.name,
|
|
103
103
|
branch: @name,
|
data/lib/better_service.rb
CHANGED
|
@@ -4,6 +4,8 @@ require "better_service/configuration"
|
|
|
4
4
|
require "better_service/errors/better_service_error"
|
|
5
5
|
require "better_service/cache_service"
|
|
6
6
|
require "better_service/presenter"
|
|
7
|
+
require "better_service/repository/base_repository"
|
|
8
|
+
require "better_service/concerns/serviceable/repository_aware"
|
|
7
9
|
require "better_service/concerns/instrumentation"
|
|
8
10
|
require "better_service/subscribers/log_subscriber"
|
|
9
11
|
require "better_service/subscribers/stats_subscriber"
|
|
@@ -8,7 +8,7 @@ module BetterService
|
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
10
|
# rails generate better_service:locale products
|
|
11
|
-
# rails generate better_service:locale
|
|
11
|
+
# rails generate better_service:locale orders
|
|
12
12
|
#
|
|
13
13
|
# This generates config/locales/products_services.en.yml with scaffolded
|
|
14
14
|
# translations for common service actions (create, update, destroy, etc.)
|
metadata
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- alessiobussolari
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '8.1'
|
|
17
20
|
- - ">="
|
|
18
21
|
- !ruby/object:Gem::Version
|
|
19
22
|
version: 8.1.1
|
|
@@ -21,6 +24,9 @@ dependencies:
|
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '8.1'
|
|
24
30
|
- - ">="
|
|
25
31
|
- !ruby/object:Gem::Version
|
|
26
32
|
version: 8.1.1
|
|
@@ -86,6 +92,7 @@ files:
|
|
|
86
92
|
- lib/better_service/concerns/serviceable/cacheable.rb
|
|
87
93
|
- lib/better_service/concerns/serviceable/messageable.rb
|
|
88
94
|
- lib/better_service/concerns/serviceable/presentable.rb
|
|
95
|
+
- lib/better_service/concerns/serviceable/repository_aware.rb
|
|
89
96
|
- lib/better_service/concerns/serviceable/transactional.rb
|
|
90
97
|
- lib/better_service/concerns/serviceable/validatable.rb
|
|
91
98
|
- lib/better_service/concerns/serviceable/viewable.rb
|
|
@@ -117,6 +124,7 @@ files:
|
|
|
117
124
|
- lib/better_service/errors/workflowable/runtime/workflow_runtime_error.rb
|
|
118
125
|
- lib/better_service/presenter.rb
|
|
119
126
|
- lib/better_service/railtie.rb
|
|
127
|
+
- lib/better_service/repository/base_repository.rb
|
|
120
128
|
- lib/better_service/services/action_service.rb
|
|
121
129
|
- lib/better_service/services/base.rb
|
|
122
130
|
- lib/better_service/services/create_service.rb
|
|
@@ -166,7 +174,6 @@ homepage: https://github.com/alessiobussolari/better_service
|
|
|
166
174
|
licenses:
|
|
167
175
|
- WTFPL
|
|
168
176
|
metadata:
|
|
169
|
-
homepage_uri: https://github.com/alessiobussolari/better_service
|
|
170
177
|
source_code_uri: https://github.com/alessiobussolari/better_service
|
|
171
178
|
changelog_uri: https://github.com/alessiobussolari/better_service/blob/main/CHANGELOG.md
|
|
172
179
|
rubygems_mfa_required: 'true'
|