better_model 2.1.0 → 3.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/README.md +96 -13
- data/lib/better_model/archivable.rb +203 -91
- data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
- data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
- data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/better_model_error.rb +9 -0
- data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
- data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
- data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
- data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
- data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
- data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
- data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
- data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
- data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
- data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
- data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
- data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
- data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
- data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
- data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
- data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
- data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
- data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
- data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
- data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
- data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
- data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
- data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
- data/lib/better_model/models/state_transition.rb +122 -0
- data/lib/better_model/models/version.rb +68 -0
- data/lib/better_model/permissible.rb +103 -52
- data/lib/better_model/predicable.rb +114 -63
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +92 -92
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +71 -53
- data/lib/better_model/stateable/guard.rb +35 -15
- data/lib/better_model/stateable/transition.rb +59 -30
- data/lib/better_model/stateable.rb +33 -15
- data/lib/better_model/statusable.rb +84 -52
- data/lib/better_model/taggable.rb +120 -75
- data/lib/better_model/traceable.rb +56 -48
- data/lib/better_model/validatable/configurator.rb +49 -172
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -5
- data/lib/generators/better_model/repository/repository_generator.rb +141 -0
- data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
- data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
- data/lib/generators/better_model/stateable/templates/README +1 -1
- metadata +44 -7
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -48
- data/lib/better_model/validatable/business_rule_validator.rb +0 -47
- data/lib/better_model/validatable/order_validator.rb +0 -77
- data/lib/better_model/version_record.rb +0 -66
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Permissible
|
|
8
|
+
# Base error class for all Permissible-related errors.
|
|
9
|
+
class PermissibleError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Predicable
|
|
8
|
+
# Base error class for all Predicable-related errors.
|
|
9
|
+
class PredicableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Searchable
|
|
8
|
+
# Base error class for all Searchable-related errors.
|
|
9
|
+
class SearchableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "stateable_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Stateable
|
|
8
|
+
class CheckFailedError < StateableError; end
|
|
9
|
+
|
|
10
|
+
# Alias for backward compatibility
|
|
11
|
+
GuardFailedError = CheckFailedError
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Stateable
|
|
8
|
+
# Base error class for all Stateable-related errors.
|
|
9
|
+
class StateableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Statusable
|
|
8
|
+
# Base error class for all Statusable-related errors.
|
|
9
|
+
class StatusableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Traceable
|
|
8
|
+
# Base error class for all Traceable-related errors.
|
|
9
|
+
class TraceableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../better_model_error"
|
|
4
|
+
|
|
5
|
+
module BetterModel
|
|
6
|
+
module Errors
|
|
7
|
+
module Validatable
|
|
8
|
+
# Base error class for all Validatable-related errors.
|
|
9
|
+
class ValidatableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Models
|
|
5
|
+
# StateTransition - Base ActiveRecord model for state transition history.
|
|
6
|
+
#
|
|
7
|
+
# This is an abstract model. Concrete classes are generated dynamically
|
|
8
|
+
# for each table (state_transitions, order_transitions, etc.).
|
|
9
|
+
#
|
|
10
|
+
# @note Table Schema
|
|
11
|
+
# t.string :transitionable_type, null: false
|
|
12
|
+
# t.integer :transitionable_id, null: false
|
|
13
|
+
# t.string :event, null: false
|
|
14
|
+
# t.string :from_state, null: false
|
|
15
|
+
# t.string :to_state, null: false
|
|
16
|
+
# t.json :metadata
|
|
17
|
+
# t.datetime :created_at, null: false
|
|
18
|
+
#
|
|
19
|
+
# @example Usage
|
|
20
|
+
# # All transitions for a model
|
|
21
|
+
# order.state_transitions
|
|
22
|
+
#
|
|
23
|
+
# @example Global queries (via dynamic classes)
|
|
24
|
+
# BetterModel::Models::StateTransitions.for_model(Order)
|
|
25
|
+
# BetterModel::Models::OrderTransitions.by_event(:confirm)
|
|
26
|
+
#
|
|
27
|
+
class StateTransition < ActiveRecord::Base
|
|
28
|
+
# Default table name (can be overridden by dynamic subclasses)
|
|
29
|
+
self.table_name = "state_transitions"
|
|
30
|
+
|
|
31
|
+
# Polymorphic association
|
|
32
|
+
belongs_to :transitionable, polymorphic: true
|
|
33
|
+
|
|
34
|
+
# Validations
|
|
35
|
+
validates :event, :from_state, :to_state, presence: true
|
|
36
|
+
|
|
37
|
+
# Scopes
|
|
38
|
+
|
|
39
|
+
# Scope for specific model.
|
|
40
|
+
#
|
|
41
|
+
# @param model_class [Class] Model class
|
|
42
|
+
# @return [ActiveRecord::Relation]
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# StateTransition.for_model(Order)
|
|
46
|
+
scope :for_model, ->(model_class) {
|
|
47
|
+
where(transitionable_type: model_class.name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Scope for specific event.
|
|
51
|
+
#
|
|
52
|
+
# @param event [Symbol, String] Event name
|
|
53
|
+
# @return [ActiveRecord::Relation]
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# StateTransition.by_event(:confirm)
|
|
57
|
+
scope :by_event, ->(event) {
|
|
58
|
+
where(event: event.to_s)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Scope for source state.
|
|
62
|
+
#
|
|
63
|
+
# @param state [Symbol, String] Source state
|
|
64
|
+
# @return [ActiveRecord::Relation]
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# StateTransition.from_state(:pending)
|
|
68
|
+
scope :from_state, ->(state) {
|
|
69
|
+
where(from_state: state.to_s)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Scope for destination state.
|
|
73
|
+
#
|
|
74
|
+
# @param state [Symbol, String] Destination state
|
|
75
|
+
# @return [ActiveRecord::Relation]
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# StateTransition.to_state(:confirmed)
|
|
79
|
+
scope :to_state, ->(state) {
|
|
80
|
+
where(to_state: state.to_s)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Scope for recent transitions.
|
|
84
|
+
#
|
|
85
|
+
# @param duration [ActiveSupport::Duration] Time duration (e.g., 7.days)
|
|
86
|
+
# @return [ActiveRecord::Relation]
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# StateTransition.recent(7.days)
|
|
90
|
+
scope :recent, ->(duration = 7.days) {
|
|
91
|
+
where("created_at >= ?", duration.ago)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Scope for transitions in a time period.
|
|
95
|
+
#
|
|
96
|
+
# @param start_time [Time, Date] Period start
|
|
97
|
+
# @param end_time [Time, Date] Period end
|
|
98
|
+
# @return [ActiveRecord::Relation]
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# StateTransition.between(1.week.ago, Time.current)
|
|
102
|
+
scope :between, ->(start_time, end_time) {
|
|
103
|
+
where(created_at: start_time..end_time)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Instance Methods
|
|
107
|
+
|
|
108
|
+
# Formatted description of the transition.
|
|
109
|
+
#
|
|
110
|
+
# @return [String] Human-readable transition description
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# transition.description # => "Order#123: pending -> confirmed (confirm)"
|
|
114
|
+
def description
|
|
115
|
+
"#{transitionable_type}##{transitionable_id}: #{from_state} -> #{to_state} (#{event})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Alias for backward compatibility
|
|
119
|
+
alias_method :to_s, :description
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Models
|
|
5
|
+
# Version model for tracking changes
|
|
6
|
+
# This is the base AR model for version history
|
|
7
|
+
# Actual table_name is set dynamically in subclasses
|
|
8
|
+
class Version < ActiveRecord::Base
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
|
|
11
|
+
# Polymorphic association to the tracked model
|
|
12
|
+
belongs_to :item, polymorphic: true, optional: true
|
|
13
|
+
|
|
14
|
+
# Optional: belongs_to user who made the change
|
|
15
|
+
# belongs_to :updated_by, class_name: "User", optional: true
|
|
16
|
+
|
|
17
|
+
# Serialize object_changes as JSON
|
|
18
|
+
# Rails handles this automatically for json/jsonb columns
|
|
19
|
+
|
|
20
|
+
# Validations
|
|
21
|
+
validates :item_type, :event, presence: true
|
|
22
|
+
validates :event, inclusion: { in: %w[created updated destroyed] }
|
|
23
|
+
|
|
24
|
+
# Scopes
|
|
25
|
+
scope :for_item, ->(item) { where(item_type: item.class.name, item_id: item.id) }
|
|
26
|
+
scope :created_events, -> { where(event: "created") }
|
|
27
|
+
scope :updated_events, -> { where(event: "updated") }
|
|
28
|
+
scope :destroyed_events, -> { where(event: "destroyed") }
|
|
29
|
+
scope :by_user, ->(user_id) { where(updated_by_id: user_id) }
|
|
30
|
+
scope :between, ->(start_time, end_time) { where(created_at: start_time..end_time) }
|
|
31
|
+
scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
|
|
32
|
+
|
|
33
|
+
# Get the change for a specific field
|
|
34
|
+
#
|
|
35
|
+
# @param field_name [Symbol, String] Field name
|
|
36
|
+
# @return [Hash, nil] Hash with :before and :after keys
|
|
37
|
+
def change_for(field_name)
|
|
38
|
+
return nil unless object_changes
|
|
39
|
+
|
|
40
|
+
field = field_name.to_s
|
|
41
|
+
return nil unless object_changes.key?(field)
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
before: object_changes[field][0],
|
|
45
|
+
after: object_changes[field][1]
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if a specific field changed in this version
|
|
50
|
+
# This method overrides ActiveRecord's changed? to accept a field_name parameter
|
|
51
|
+
#
|
|
52
|
+
# @param field_name [Symbol, String, nil] Field name (if nil, calls ActiveRecord's changed?)
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def changed?(field_name = nil)
|
|
55
|
+
return super() if field_name.nil?
|
|
56
|
+
|
|
57
|
+
object_changes&.key?(field_name.to_s) || false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get list of changed fields
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<String>]
|
|
63
|
+
def changed_fields
|
|
64
|
+
object_changes&.keys || []
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|