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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43311885d24f9c9001eba91737135c5fa8787cc42e5f5e7beaaf912e2ae177d0
|
|
4
|
+
data.tar.gz: cc80f44ca0c7c5992302175d949ef237d3b0f4d2274c9b1d13bba6398f6dad6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77874dd1ad5221c36a5498b74810f1812e978a984f24e35e50dcdfba0019899d4afe742bf431c68a3b5c94e30a3fe1e9c0f7d3e16e16722044ec4f91b2538b57
|
|
7
|
+
data.tar.gz: 11f774a0981cd7bc2ec1f307a63a8d0e3f91dc7431b58e262cd9bb5edc858e9548c8f51ea190e7e50a8b17c88965cdbc6d0bbb36e39e819fef9d7464ffb032de
|
data/README.md
CHANGED
|
@@ -53,20 +53,20 @@ class Article < ApplicationRecord
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# 6. VALIDATABLE - Declarative validation system (opt-in)
|
|
56
|
+
register_complex_validation :published_requirements do
|
|
57
|
+
return unless status == "published"
|
|
58
|
+
errors.add(:published_at, "must be present for published articles") if published_at.blank?
|
|
59
|
+
end
|
|
60
|
+
|
|
56
61
|
validatable do
|
|
57
62
|
# Basic validations
|
|
58
63
|
check :title, :content, presence: true
|
|
59
64
|
|
|
60
|
-
# Conditional validations
|
|
61
|
-
|
|
62
|
-
check :published_at, presence: true
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Cross-field validations
|
|
66
|
-
validate_order :starts_at, :before, :ends_at
|
|
65
|
+
# Conditional validations using Rails options
|
|
66
|
+
check :published_at, presence: true, if: -> { status == "published" }
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
|
|
68
|
+
# Complex validations for business rules
|
|
69
|
+
check_complex :published_requirements
|
|
70
70
|
|
|
71
71
|
# Validation groups (multi-step forms)
|
|
72
72
|
validation_group :step1, [:title, :content]
|
|
@@ -335,6 +335,90 @@ rails db:migrate
|
|
|
335
335
|
# See model setup instructions after running each generator
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
+
### Repository Generator
|
|
339
|
+
|
|
340
|
+
Create repository classes that implement the Repository Pattern:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Basic usage - creates model repository and ApplicationRepository
|
|
344
|
+
rails g better_model:repository Article
|
|
345
|
+
|
|
346
|
+
# Custom path
|
|
347
|
+
rails g better_model:repository Article --path app/services/repositories
|
|
348
|
+
|
|
349
|
+
# Skip ApplicationRepository creation
|
|
350
|
+
rails g better_model:repository Article --skip-base
|
|
351
|
+
|
|
352
|
+
# With namespace
|
|
353
|
+
rails g better_model:repository Article --namespace Admin
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Generated repository includes:**
|
|
357
|
+
- Repository class inheriting from `ApplicationRepository` (or `BetterModel::Repositable::BaseRepository` with `--skip-base`)
|
|
358
|
+
- Modern Ruby 3 endless method syntax: `def model_class = Article`
|
|
359
|
+
- Commented examples of custom query methods
|
|
360
|
+
- Integration with BetterModel's Searchable, Predicable, and Sortable features
|
|
361
|
+
- Auto-generated documentation of available predicates and sort scopes (if model uses BetterModel)
|
|
362
|
+
|
|
363
|
+
**ApplicationRepository (generated once):**
|
|
364
|
+
- Base class for all repositories in your application
|
|
365
|
+
- Inherits from `BetterModel::Repositable::BaseRepository`
|
|
366
|
+
- Can be customized with application-wide repository behaviors
|
|
367
|
+
|
|
368
|
+
**Example usage:**
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
# app/repositories/article_repository.rb
|
|
372
|
+
class ArticleRepository < ApplicationRepository
|
|
373
|
+
def model_class = Article
|
|
374
|
+
|
|
375
|
+
def published
|
|
376
|
+
search({ status_eq: "published" })
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def recent(days: 7)
|
|
380
|
+
search({ created_at_gteq: days.days.ago }, order_scope: { field: :created_at, direction: :desc })
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def popular(min_views: 100)
|
|
384
|
+
search({ view_count_gteq: min_views }, orders: [:sort_view_count_desc])
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def search_text(query)
|
|
388
|
+
search({
|
|
389
|
+
or: [
|
|
390
|
+
{ title_i_cont: query },
|
|
391
|
+
{ content_i_cont: query }
|
|
392
|
+
]
|
|
393
|
+
})
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# In your controllers/services
|
|
398
|
+
repo = ArticleRepository.new
|
|
399
|
+
articles = repo.published
|
|
400
|
+
popular = repo.popular(min_views: 200)
|
|
401
|
+
results = repo.search({ status_eq: "published" }, page: 1, per_page: 20)
|
|
402
|
+
article = repo.search({ id_eq: 1 }, limit: 1)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**BaseRepository features:**
|
|
406
|
+
- `search(predicates, page:, per_page:, includes:, joins:, order:, order_scope:, limit:)` - Main search method
|
|
407
|
+
- `find(id)`, `find_by(...)` - Standard ActiveRecord finders
|
|
408
|
+
- `create(attrs)`, `create!(attrs)` - Create records
|
|
409
|
+
- `build(attrs)` - Build new instances
|
|
410
|
+
- `update(id, attrs)` - Update records
|
|
411
|
+
- `delete(id)` - Delete records
|
|
412
|
+
- `where(...)`, `all`, `count`, `exists?` - Basic ActiveRecord methods
|
|
413
|
+
|
|
414
|
+
**Search method parameters:**
|
|
415
|
+
- **predicates**: Hash of BetterModel predicates (e.g., `{ status_eq: "published", view_count_gt: 100 }`)
|
|
416
|
+
- **page/per_page**: Pagination (default: page 1, 20 per page)
|
|
417
|
+
- **includes/joins**: Eager loading and associations
|
|
418
|
+
- **order**: SQL order clause
|
|
419
|
+
- **order_scope**: BetterModel sort scope (e.g., `{ field: :published_at, direction: :desc }`)
|
|
420
|
+
- **limit**: Result limit (Integer for limit, `:default` for pagination, `nil` for all results)
|
|
421
|
+
|
|
338
422
|
## 📋 Features Overview
|
|
339
423
|
|
|
340
424
|
BetterModel provides ten powerful concerns that work seamlessly together:
|
|
@@ -450,11 +534,10 @@ Define validations declaratively with support for conditional rules, cross-field
|
|
|
450
534
|
|
|
451
535
|
**🎯 Key Benefits:**
|
|
452
536
|
- 🎛️ Opt-in activation: only enabled when explicitly configured
|
|
453
|
-
- ✨ Declarative DSL for
|
|
454
|
-
-
|
|
455
|
-
- 🔗 Cross-field validations: `validate_order` for date/number comparisons
|
|
456
|
-
- 💼 Business rules: delegate complex logic to custom methods
|
|
537
|
+
- ✨ Declarative DSL with `check` method for basic validations
|
|
538
|
+
- 🔗 Complex validations: reusable validation blocks for cross-field and business logic
|
|
457
539
|
- 📋 Validation groups: partial validation for multi-step forms
|
|
540
|
+
- 🔀 Conditional validations using Rails `if:` / `unless:` options
|
|
458
541
|
- 🔒 Thread-safe with immutable configuration
|
|
459
542
|
|
|
460
543
|
**[📖 Full Documentation →](docs/validatable.md)**
|
|
@@ -1,41 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/archivable/archivable_error"
|
|
4
|
+
require_relative "errors/archivable/already_archived_error"
|
|
5
|
+
require_relative "errors/archivable/not_archived_error"
|
|
6
|
+
require_relative "errors/archivable/not_enabled_error"
|
|
7
|
+
require_relative "errors/archivable/configuration_error"
|
|
8
|
+
|
|
9
|
+
# Archivable - Declarative archiving system for Rails models.
|
|
4
10
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
11
|
+
# This concern enables archiving and restoring records using a simple, declarative DSL
|
|
12
|
+
# with support for predicates and scopes.
|
|
7
13
|
#
|
|
8
|
-
#
|
|
9
|
-
# # Opzione 1: Generator automatico (raccomandato)
|
|
14
|
+
# @example Quick Setup - Option 1: Automatic Generator (Recommended)
|
|
10
15
|
# rails g better_model:archivable Article --with-tracking
|
|
11
16
|
# rails db:migrate
|
|
12
17
|
#
|
|
13
|
-
#
|
|
18
|
+
# @example Quick Setup - Option 2: Manual Migration
|
|
14
19
|
# rails g migration AddArchivableToArticles archived_at:datetime archived_by_id:integer archive_reason:string
|
|
15
20
|
# rails db:migrate
|
|
16
21
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
22
|
+
# @note OPT-IN APPROACH
|
|
23
|
+
# Archiving is not enabled automatically. You must explicitly call
|
|
24
|
+
# `archivable do...end` in your model to activate it.
|
|
19
25
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
26
|
+
# @note HYBRID APPROACH
|
|
27
|
+
# Uses existing predicates (archived_at_present, archived_at_null, etc.)
|
|
28
|
+
# and provides semantic aliases (archived, not_archived) for better readability.
|
|
22
29
|
#
|
|
23
|
-
#
|
|
24
|
-
# - archived_at (datetime) - REQUIRED
|
|
25
|
-
# - archived_by_id (integer) - OPTIONAL (
|
|
26
|
-
# - archive_reason (string) - OPTIONAL (
|
|
30
|
+
# @note DATABASE REQUIREMENTS
|
|
31
|
+
# - archived_at (datetime) - REQUIRED
|
|
32
|
+
# - archived_by_id (integer) - OPTIONAL (for user tracking)
|
|
33
|
+
# - archive_reason (string) - OPTIONAL (for reasoning)
|
|
27
34
|
#
|
|
28
|
-
#
|
|
35
|
+
# @example Basic Model Setup
|
|
29
36
|
# class Article < ApplicationRecord
|
|
30
37
|
# include BetterModel
|
|
31
38
|
#
|
|
32
|
-
# #
|
|
39
|
+
# # Enable archivable (opt-in)
|
|
33
40
|
# archivable do
|
|
34
|
-
# skip_archived_by_default true #
|
|
41
|
+
# skip_archived_by_default true # Optional: hide archived records by default
|
|
35
42
|
# end
|
|
36
43
|
# end
|
|
37
44
|
#
|
|
38
|
-
#
|
|
45
|
+
# @example Instance Methods Usage
|
|
39
46
|
# article.archive! # Archive the record
|
|
40
47
|
# article.archive!(by: user) # Archive with user tracking
|
|
41
48
|
# article.archive!(reason: "Outdated") # Archive with reason
|
|
@@ -43,22 +50,22 @@
|
|
|
43
50
|
# article.archived? # Check if archived
|
|
44
51
|
# article.active? # Check if not archived
|
|
45
52
|
#
|
|
46
|
-
#
|
|
53
|
+
# @example Semantic Scopes
|
|
47
54
|
# Article.archived # Find archived records
|
|
48
55
|
# Article.not_archived # Find active records
|
|
49
56
|
# Article.archived_only # Bypass default scope
|
|
50
57
|
#
|
|
51
|
-
#
|
|
58
|
+
# @example Powerful Predicates (Auto-generated)
|
|
52
59
|
# Article.archived_at_within(7.days) # Archived in last 7 days
|
|
53
60
|
# Article.archived_at_today # Archived today
|
|
54
61
|
# Article.archived_at_between(start, end) # Archived in range
|
|
55
62
|
#
|
|
56
|
-
#
|
|
63
|
+
# @example Helper Methods
|
|
57
64
|
# Article.archived_today # Alias for archived_at_today
|
|
58
65
|
# Article.archived_this_week # Alias for archived_at_this_week
|
|
59
66
|
# Article.archived_recently(7.days) # Alias for archived_at_within
|
|
60
67
|
#
|
|
61
|
-
#
|
|
68
|
+
# @example Integration with Searchable
|
|
62
69
|
# Article.search({ archived_at_null: true, status_eq: "published" })
|
|
63
70
|
#
|
|
64
71
|
module BetterModel
|
|
@@ -66,113 +73,177 @@ module BetterModel
|
|
|
66
73
|
extend ActiveSupport::Concern
|
|
67
74
|
|
|
68
75
|
included do
|
|
69
|
-
#
|
|
76
|
+
# Validate ActiveRecord inheritance
|
|
70
77
|
unless ancestors.include?(ActiveRecord::Base)
|
|
71
|
-
raise
|
|
78
|
+
raise BetterModel::Errors::Archivable::ConfigurationError, "Invalid configuration"
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
#
|
|
81
|
+
# Archivable configuration (opt-in)
|
|
75
82
|
class_attribute :archivable_enabled, default: false
|
|
76
83
|
class_attribute :archivable_config, default: {}.freeze
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
class_methods do
|
|
80
|
-
# DSL
|
|
87
|
+
# DSL to enable and configure archivable (OPT-IN).
|
|
88
|
+
#
|
|
89
|
+
# This method activates archivable functionality on your model. It automatically
|
|
90
|
+
# defines predicates and sorts on archived_at, creates semantic scope aliases,
|
|
91
|
+
# and optionally configures default scoping behavior.
|
|
92
|
+
#
|
|
93
|
+
# @yield [configurator] Optional configuration block
|
|
94
|
+
# @raise [BetterModel::Errors::Archivable::ConfigurationError] If archived_at column doesn't exist
|
|
81
95
|
#
|
|
82
|
-
# @example
|
|
96
|
+
# @example Basic activation
|
|
83
97
|
# archivable
|
|
84
98
|
#
|
|
85
|
-
# @example
|
|
99
|
+
# @example With configuration
|
|
86
100
|
# archivable do
|
|
87
101
|
# skip_archived_by_default true
|
|
88
102
|
# end
|
|
89
103
|
def archivable(&block)
|
|
90
|
-
#
|
|
104
|
+
# Validate that archived_at exists
|
|
91
105
|
unless column_names.include?("archived_at")
|
|
92
|
-
raise
|
|
93
|
-
"Archivable requires an 'archived_at' datetime column. " \
|
|
94
|
-
"Add it with: rails g migration AddArchivedAtTo#{table_name.classify.pluralize} archived_at:datetime"
|
|
106
|
+
raise BetterModel::Errors::Archivable::ConfigurationError, "Invalid configuration"
|
|
95
107
|
end
|
|
96
108
|
|
|
97
|
-
#
|
|
109
|
+
# Enable archivable
|
|
98
110
|
self.archivable_enabled = true
|
|
99
111
|
|
|
100
|
-
#
|
|
112
|
+
# Define predicates on archived_at (opt-in!)
|
|
101
113
|
predicates :archived_at unless predicable_field?(:archived_at)
|
|
102
114
|
|
|
103
|
-
#
|
|
115
|
+
# Define sorting on archived_at (opt-in!)
|
|
104
116
|
sort :archived_at unless sortable_field?(:archived_at)
|
|
105
117
|
|
|
106
|
-
#
|
|
118
|
+
# Define semantic scope aliases (hybrid approach)
|
|
107
119
|
scope :archived, -> { archived_at_present(true) }
|
|
108
120
|
scope :not_archived, -> { archived_at_null(true) }
|
|
109
121
|
|
|
110
|
-
#
|
|
122
|
+
# Configure if block provided
|
|
111
123
|
if block_given?
|
|
112
124
|
configurator = ArchivableConfigurator.new(self)
|
|
113
125
|
configurator.instance_eval(&block)
|
|
114
126
|
self.archivable_config = configurator.to_h.freeze
|
|
115
127
|
|
|
116
|
-
#
|
|
128
|
+
# Apply default scope ONLY if configured
|
|
117
129
|
if archivable_config[:skip_archived_by_default]
|
|
118
130
|
default_scope -> { where(archived_at: nil) }
|
|
119
131
|
end
|
|
120
132
|
end
|
|
121
133
|
end
|
|
122
134
|
|
|
123
|
-
#
|
|
135
|
+
# Find ONLY archived records, bypassing default scope.
|
|
124
136
|
#
|
|
125
137
|
# @return [ActiveRecord::Relation]
|
|
138
|
+
# @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
|
|
139
|
+
#
|
|
140
|
+
# @example
|
|
141
|
+
# Article.archived_only
|
|
126
142
|
def archived_only
|
|
127
|
-
|
|
143
|
+
unless archivable_enabled?
|
|
144
|
+
raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
|
|
145
|
+
end
|
|
128
146
|
unscoped.archived
|
|
129
147
|
end
|
|
130
148
|
|
|
131
|
-
#
|
|
149
|
+
# Find records archived today.
|
|
150
|
+
#
|
|
151
|
+
# Helper alias for archived_at_today predicate.
|
|
152
|
+
#
|
|
153
|
+
# @return [ActiveRecord::Relation]
|
|
154
|
+
# @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
|
|
155
|
+
#
|
|
156
|
+
# @example
|
|
157
|
+
# Article.archived_today
|
|
132
158
|
def archived_today
|
|
133
|
-
|
|
159
|
+
unless archivable_enabled?
|
|
160
|
+
raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
|
|
161
|
+
end
|
|
134
162
|
archived_at_today
|
|
135
163
|
end
|
|
136
164
|
|
|
137
|
-
#
|
|
165
|
+
# Find records archived this week.
|
|
166
|
+
#
|
|
167
|
+
# Helper alias for archived_at_this_week predicate.
|
|
168
|
+
#
|
|
169
|
+
# @return [ActiveRecord::Relation]
|
|
170
|
+
# @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
|
|
171
|
+
#
|
|
172
|
+
# @example
|
|
173
|
+
# Article.archived_this_week
|
|
138
174
|
def archived_this_week
|
|
139
|
-
|
|
175
|
+
unless archivable_enabled?
|
|
176
|
+
raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
|
|
177
|
+
end
|
|
140
178
|
archived_at_this_week
|
|
141
179
|
end
|
|
142
180
|
|
|
143
|
-
#
|
|
181
|
+
# Find records archived within the specified duration.
|
|
144
182
|
#
|
|
145
|
-
#
|
|
183
|
+
# Helper alias for archived_at_within predicate.
|
|
184
|
+
#
|
|
185
|
+
# @param duration [ActiveSupport::Duration] Time duration (e.g., 7.days)
|
|
146
186
|
# @return [ActiveRecord::Relation]
|
|
187
|
+
# @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
|
|
188
|
+
#
|
|
189
|
+
# @example Find records archived in the last 7 days
|
|
190
|
+
# Article.archived_recently(7.days)
|
|
191
|
+
#
|
|
192
|
+
# @example Find records archived in the last month
|
|
193
|
+
# Article.archived_recently(1.month)
|
|
147
194
|
def archived_recently(duration = 7.days)
|
|
148
|
-
|
|
195
|
+
unless archivable_enabled?
|
|
196
|
+
raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
|
|
197
|
+
end
|
|
149
198
|
archived_at_within(duration)
|
|
150
199
|
end
|
|
151
200
|
|
|
152
|
-
#
|
|
201
|
+
# Check if archivable is enabled on this model.
|
|
153
202
|
#
|
|
154
|
-
# @return [Boolean]
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
# @return [Boolean] true if archivable is enabled, false otherwise
|
|
204
|
+
#
|
|
205
|
+
# @example
|
|
206
|
+
# Article.archivable_enabled? # => true
|
|
207
|
+
def archivable_enabled? = archivable_enabled == true
|
|
158
208
|
end
|
|
159
209
|
|
|
160
|
-
#
|
|
210
|
+
# Instance Methods
|
|
161
211
|
|
|
162
|
-
#
|
|
212
|
+
# Archive this record.
|
|
213
|
+
#
|
|
214
|
+
# Sets archived_at to current time and optionally tracks the user and reason.
|
|
215
|
+
# Bypasses validations when saving.
|
|
216
|
+
#
|
|
217
|
+
# @param by [Integer, Object, nil] User ID or user object (optional)
|
|
218
|
+
# @param reason [String, nil] Reason for archiving (optional)
|
|
219
|
+
# @return [self] Returns self for method chaining
|
|
220
|
+
# @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
|
|
221
|
+
# @raise [BetterModel::Errors::Archivable::AlreadyArchivedError] If already archived
|
|
222
|
+
#
|
|
223
|
+
# @example Basic archiving
|
|
224
|
+
# article.archive!
|
|
163
225
|
#
|
|
164
|
-
# @
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
# @
|
|
226
|
+
# @example Archive with user tracking
|
|
227
|
+
# article.archive!(by: current_user)
|
|
228
|
+
# article.archive!(by: user.id)
|
|
229
|
+
#
|
|
230
|
+
# @example Archive with reason
|
|
231
|
+
# article.archive!(reason: "Outdated content")
|
|
232
|
+
#
|
|
233
|
+
# @example Archive with both user and reason
|
|
234
|
+
# article.archive!(by: current_user, reason: "Compliance violation")
|
|
169
235
|
def archive!(by: nil, reason: nil)
|
|
170
|
-
|
|
171
|
-
|
|
236
|
+
unless self.class.archivable_enabled?
|
|
237
|
+
raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if archived?
|
|
241
|
+
raise BetterModel::Errors::Archivable::AlreadyArchivedError, "Record is already archived"
|
|
242
|
+
end
|
|
172
243
|
|
|
173
244
|
self.archived_at = Time.current
|
|
174
245
|
|
|
175
|
-
# Set archived_by_id:
|
|
246
|
+
# Set archived_by_id: accepts both ID and objects with .id
|
|
176
247
|
if respond_to?(:archived_by_id=) && by.present?
|
|
177
248
|
self.archived_by_id = by.respond_to?(:id) ? by.id : by
|
|
178
249
|
end
|
|
@@ -183,14 +254,25 @@ module BetterModel
|
|
|
183
254
|
self
|
|
184
255
|
end
|
|
185
256
|
|
|
186
|
-
#
|
|
257
|
+
# Restore archived record.
|
|
258
|
+
#
|
|
259
|
+
# Clears archived_at, archived_by_id, and archive_reason.
|
|
260
|
+
# Bypasses validations when saving.
|
|
187
261
|
#
|
|
188
|
-
# @return [self]
|
|
189
|
-
# @raise [
|
|
190
|
-
# @raise [NotArchivedError]
|
|
262
|
+
# @return [self] Returns self for method chaining
|
|
263
|
+
# @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
|
|
264
|
+
# @raise [BetterModel::Errors::Archivable::NotArchivedError] If not archived
|
|
265
|
+
#
|
|
266
|
+
# @example
|
|
267
|
+
# article.restore!
|
|
191
268
|
def restore!
|
|
192
|
-
|
|
193
|
-
|
|
269
|
+
unless self.class.archivable_enabled?
|
|
270
|
+
raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
unless archived?
|
|
274
|
+
raise BetterModel::Errors::Archivable::NotArchivedError, "Record is not archived"
|
|
275
|
+
end
|
|
194
276
|
|
|
195
277
|
self.archived_at = nil
|
|
196
278
|
self.archived_by_id = nil if respond_to?(:archived_by_id=)
|
|
@@ -200,26 +282,47 @@ module BetterModel
|
|
|
200
282
|
self
|
|
201
283
|
end
|
|
202
284
|
|
|
203
|
-
#
|
|
285
|
+
# Check if record is archived.
|
|
286
|
+
#
|
|
287
|
+
# @return [Boolean] true if archived, false otherwise
|
|
204
288
|
#
|
|
205
|
-
# @
|
|
289
|
+
# @example
|
|
290
|
+
# article.archived? # => true
|
|
206
291
|
def archived?
|
|
207
292
|
return false unless self.class.archivable_enabled?
|
|
208
293
|
archived_at.present?
|
|
209
294
|
end
|
|
210
295
|
|
|
211
|
-
#
|
|
296
|
+
# Check if record is active (not archived).
|
|
212
297
|
#
|
|
213
|
-
# @return [Boolean]
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
298
|
+
# @return [Boolean] true if active, false otherwise
|
|
299
|
+
#
|
|
300
|
+
# @example
|
|
301
|
+
# article.active? # => true
|
|
302
|
+
def active? = !archived?
|
|
217
303
|
|
|
218
|
-
# Override as_json
|
|
304
|
+
# Override as_json to include archive information.
|
|
305
|
+
#
|
|
306
|
+
# Optionally includes detailed archive metadata when requested.
|
|
219
307
|
#
|
|
220
|
-
# @param options [Hash]
|
|
308
|
+
# @param options [Hash] Options for as_json
|
|
221
309
|
# @option options [Boolean] :include_archive_info Include archive metadata
|
|
222
|
-
# @return [Hash]
|
|
310
|
+
# @return [Hash] JSON representation
|
|
311
|
+
#
|
|
312
|
+
# @example Basic JSON
|
|
313
|
+
# article.as_json
|
|
314
|
+
#
|
|
315
|
+
# @example With archive info
|
|
316
|
+
# article.as_json(include_archive_info: true)
|
|
317
|
+
# # => {
|
|
318
|
+
# # ...,
|
|
319
|
+
# # "archive_info" => {
|
|
320
|
+
# # "archived" => true,
|
|
321
|
+
# # "archived_at" => "2025-01-15T10:30:00Z",
|
|
322
|
+
# # "archived_by_id" => 42,
|
|
323
|
+
# # "archive_reason" => "Outdated"
|
|
324
|
+
# # }
|
|
325
|
+
# # }
|
|
223
326
|
def as_json(options = {})
|
|
224
327
|
result = super
|
|
225
328
|
|
|
@@ -236,21 +339,18 @@ module BetterModel
|
|
|
236
339
|
end
|
|
237
340
|
end
|
|
238
341
|
|
|
239
|
-
# Errori custom
|
|
240
|
-
class ArchivableError < StandardError; end
|
|
241
|
-
class AlreadyArchivedError < ArchivableError; end
|
|
242
|
-
class NotArchivedError < ArchivableError; end
|
|
243
342
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
# Configurator per archivable DSL
|
|
343
|
+
# Configurator for archivable DSL.
|
|
344
|
+
#
|
|
345
|
+
# Internal class used to configure archivable behavior through the DSL block.
|
|
346
|
+
#
|
|
347
|
+
# @api private
|
|
251
348
|
class ArchivableConfigurator
|
|
252
349
|
attr_reader :config
|
|
253
350
|
|
|
351
|
+
# Initialize a new configurator.
|
|
352
|
+
#
|
|
353
|
+
# @param model_class [Class] The model class being configured
|
|
254
354
|
def initialize(model_class)
|
|
255
355
|
@model_class = model_class
|
|
256
356
|
@config = {
|
|
@@ -258,13 +358,25 @@ module BetterModel
|
|
|
258
358
|
}
|
|
259
359
|
end
|
|
260
360
|
|
|
261
|
-
#
|
|
361
|
+
# Configure default scope to hide archived records.
|
|
362
|
+
#
|
|
363
|
+
# When enabled, adds a default scope that filters out archived records.
|
|
364
|
+
# Use Model.unscoped or Model.archived_only to access archived records.
|
|
262
365
|
#
|
|
263
|
-
# @param value [Boolean] true
|
|
366
|
+
# @param value [Boolean] true to hide archived records by default
|
|
367
|
+
#
|
|
368
|
+
# @example
|
|
369
|
+
# archivable do
|
|
370
|
+
# skip_archived_by_default true
|
|
371
|
+
# end
|
|
264
372
|
def skip_archived_by_default(value)
|
|
265
373
|
@config[:skip_archived_by_default] = !!value
|
|
266
374
|
end
|
|
267
375
|
|
|
376
|
+
# Convert configuration to hash.
|
|
377
|
+
#
|
|
378
|
+
# @return [Hash] Configuration hash
|
|
379
|
+
# @api private
|
|
268
380
|
def to_h
|
|
269
381
|
@config
|
|
270
382
|
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 Archivable
|
|
8
|
+
# Base error class for all Archivable-related errors.
|
|
9
|
+
class ArchivableError < BetterModelError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|