sequel-soft-deletes 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b90ce7450ab370ffd9fb26653e35597ebd462bdaf660a4572a9b8ff63cfae9a
4
+ data.tar.gz: 6513ffde924e7dbfac20b26ad37aab7d1ebf08bf453c1adc279053984362334e
5
+ SHA512:
6
+ metadata.gz: c6db370861d3c3430df66538c5c7270829c0c5a60c95f201436ce90b59272697e0d4cad07829b32fab1405ce9776b5428138272d17ebb1a1b77e8f86056a47fe
7
+ data.tar.gz: 0d5020c98215beb773b5f14fb781a15a78a5820294216622da6e701bb74a48c4e3eaf6510371579bc77c6a0b9f8d1ae0421b8c64fb495385e0de43eefeb1de51
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sequel"
4
+ require "sequel/model"
5
+
6
+ # Plugin for adding soft-delete to a model.
7
+ #
8
+ # == Example
9
+ #
10
+ # Defining a model class with a timestamp as the deletion flag:
11
+ #
12
+ # class ACME::Customer < Sequel::Model(:customers)
13
+ # plugin :soft_deletes, column: :deleted_at
14
+ #
15
+ # And in the schema:
16
+ # create_table( :customers ) do
17
+ # primary_key :id
18
+ # timestamptz :deleted_at
19
+ # end
20
+ #
21
+ module Sequel::Plugins::SoftDeletes
22
+ VERSION = "0.1.2"
23
+
24
+ # Default plugin options
25
+ DEFAULT_OPTIONS = {
26
+ column: :soft_deleted_at,
27
+ omit_by_default: false,
28
+ }.freeze
29
+
30
+ def self.configure(model, opts=DEFAULT_OPTIONS)
31
+ opts = DEFAULT_OPTIONS.merge(opts)
32
+ column = opts[:column]
33
+ model.soft_delete_column = column
34
+ model.set_dataset(model.where(column => nil)) if opts[:omit_by_default]
35
+ end
36
+
37
+ module DatasetMethods
38
+ def soft_deleted
39
+ column = self.model.soft_delete_column
40
+ exclude(column => nil)
41
+ end
42
+
43
+ def not_soft_deleted
44
+ column = self.model.soft_delete_column
45
+ where(column => nil)
46
+ end
47
+ end
48
+
49
+ # Methods to extend Model classes with.
50
+ module ClassMethods
51
+ ##
52
+ # The Array of field which are images, as Symbols
53
+ attr_accessor :soft_delete_column
54
+ end
55
+
56
+ # Methods to extend Model instances with.
57
+ module InstanceMethods
58
+ ### Returns +true+ if this object should be considered deleted.
59
+ def soft_deleted?
60
+ column = self.class.soft_delete_column
61
+ return self[column] ? true : false
62
+ end
63
+
64
+ alias is_soft_deleted? soft_deleted?
65
+
66
+ ### Returns +true+ if the object is soft-deletable. By default, an
67
+ ### object is soft-deletable if it has no +soft_deletion_blockers+.
68
+ def soft_deletable?
69
+ return self.soft_deletion_blockers.empty?
70
+ end
71
+
72
+ ### Soft-delete this instance.
73
+ def soft_delete
74
+ column = self.class.soft_delete_column
75
+
76
+ self.db.transaction do
77
+ supered_from_around = false
78
+ self.around_soft_delete do
79
+ supered_from_around = true
80
+ raise_hook_failure(:before_soft_delete) unless self.before_soft_delete
81
+
82
+ self.update(column => Time.now)
83
+
84
+ self.after_soft_delete
85
+ end
86
+ raise_hook_failure(:around_soft_delete) unless supered_from_around
87
+ end
88
+ end
89
+
90
+ ### Returns an array of conditions preventing soft-deletion. Default is an empty array.
91
+ def soft_deletion_blockers
92
+ return []
93
+ end
94
+
95
+ ### Remove soft-deletion blockers. Default soft-deletion raises NotImplementedError.
96
+ def remove_soft_deletion_blockers
97
+ raise NotImplementedError
98
+ end
99
+
100
+ ### Default 'before' soft-delete hook checks if object is soft-deletable.
101
+ ### Aborts soft-deletion if it returns false.
102
+ def before_soft_delete
103
+ return self.soft_deletable?
104
+ end
105
+
106
+ ### Default (empty) 'around' soft-delete model hook.
107
+ def around_soft_delete
108
+ yield
109
+ end
110
+
111
+ ### Default (empty) 'after' soft-delete hook.
112
+ def after_soft_delete; end
113
+
114
+ ### Return the information for the soft-deletes column.
115
+ def soft_delete_column
116
+ return self.class.schema.columns.find do |col|
117
+ col[:name] == self.class.soft_delete_column
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/integer/time"
4
+ require "sequel/plugins/soft_deletes"
5
+ require "sequel"
6
+ require "sqlite3"
7
+
8
+ RSpec.describe Sequel::Plugins::SoftDeletes, :db do
9
+ before(:each) do
10
+ @db = Sequel.sqlite
11
+ end
12
+ after(:each) do
13
+ @db.disconnect
14
+ end
15
+
16
+ let(:table_name) { :soft_deletes_test }
17
+
18
+ it "sets the soft-delete column to :soft_deleted_at if none is specified" do
19
+ @db.create_table(:soft_deletes_test) do
20
+ primary_key :id
21
+ time :deleted_at
22
+ end
23
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
24
+ mc.plugin(:soft_deletes)
25
+ expect(mc.soft_delete_column).to eq(:soft_deleted_at)
26
+ end
27
+
28
+ it "allows the class to override the soft-delete column" do
29
+ @db.create_table(:soft_deletes_test) do
30
+ primary_key :id
31
+ time :deleted_at
32
+ end
33
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
34
+ mc.plugin(:soft_deletes, column: :deleted_at)
35
+ expect(mc.soft_delete_column).to eq(:deleted_at)
36
+ end
37
+
38
+ it "defines a #soft_delete method on extended model instances" do
39
+ @db.create_table(:soft_deletes_test) do
40
+ primary_key :id
41
+ time :deleted_at
42
+ end
43
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
44
+ mc.plugin(:soft_deletes)
45
+ @m = mc.new
46
+
47
+ expect(@m).to respond_to(:soft_delete)
48
+ end
49
+
50
+ context "extended model classes with a timestamp soft-delete column" do
51
+ before do
52
+ @db.create_table(:soft_deletes_test) do
53
+ primary_key :id
54
+ time :deleted_at
55
+ end
56
+ @c = Class.new(Sequel::Model(@db[:soft_deletes_test]))
57
+ @c.plugin(:soft_deletes, column: :deleted_at)
58
+ @m = @c.create
59
+ end
60
+
61
+ it "sets its column to 'now' when soft-deleted" do
62
+ @m.soft_delete
63
+ expect(@m.deleted_at).to be_a(Time)
64
+ expect(@m.deleted_at).to be_within(5.seconds).of(Time.now)
65
+ end
66
+
67
+ it "sets up a subset for selecting (or de-selecting) soft-deleted rows" do
68
+ expect(@c.dataset.soft_deleted).to be_a(Sequel::Dataset)
69
+ expect(@c.dataset.not_soft_deleted).to be_a(Sequel::Dataset)
70
+
71
+ expect(@c.dataset.soft_deleted.all).not_to include(@m)
72
+ expect(@c.dataset.not_soft_deleted.all).to include(@m)
73
+
74
+ @m.soft_delete
75
+ expect(@c.dataset.soft_deleted.all).to include(@m)
76
+ expect(@c.dataset.not_soft_deleted.all).not_to include(@m)
77
+ end
78
+ end
79
+
80
+ context "extended model classes with a 'before' soft-delete hook" do
81
+ before do
82
+ @db.create_table(:soft_deletes_test) do
83
+ primary_key :id
84
+ time :deleted_at
85
+ end
86
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
87
+ mc.class_eval do
88
+ attr_accessor :hook_body
89
+
90
+ def before_soft_delete
91
+ self.hook_body.call
92
+ end
93
+ end
94
+ mc.plugin(:soft_deletes, column: :deleted_at)
95
+ @m = mc.new
96
+ end
97
+
98
+ it "has its hook called whenever an instance is soft-deleted" do
99
+ called = false
100
+ @m.hook_body = lambda do
101
+ called = true
102
+ end
103
+ @m.soft_delete
104
+
105
+ expect(@m).to be_is_soft_deleted
106
+ expect(called).to eq(true)
107
+ end
108
+
109
+ it "is not soft-deleted if its hook returns false" do
110
+ @m.hook_body = lambda do
111
+ false
112
+ end
113
+
114
+ expect do
115
+ @m.soft_delete
116
+ end.to raise_error(Sequel::HookFailed, /before_soft_delete hook failed/i)
117
+
118
+ expect(@m).not_to be_soft_deleted
119
+ end
120
+ end
121
+
122
+ context "extended model classes with an 'after' soft-delete hook" do
123
+ before do
124
+ @db.create_table(:soft_deletes_test) do
125
+ primary_key :id
126
+ time :deleted_at
127
+ end
128
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
129
+ mc.class_eval do
130
+ attr_accessor :hook_body
131
+
132
+ def after_soft_delete
133
+ self.hook_body.call
134
+ end
135
+ end
136
+ mc.plugin(:soft_deletes, column: :deleted_at)
137
+ @m = mc.new
138
+ end
139
+
140
+ it "has its hook called whenever an instance is soft-deleted" do
141
+ called = false
142
+ @m.hook_body = lambda do
143
+ called = true
144
+ end
145
+ @m.soft_delete
146
+
147
+ expect(@m).to be_is_soft_deleted
148
+ expect(called).to eq(true)
149
+ end
150
+
151
+ it "is still soft-deleted even if its hook returns false" do
152
+ @m.hook_body = lambda do
153
+ false
154
+ end
155
+
156
+ expect { @m.soft_delete }.not_to raise_error
157
+
158
+ expect(@m).to be_is_soft_deleted
159
+ end
160
+ end
161
+
162
+ context "extended model classes with an 'around' soft-delete hook" do
163
+ before do
164
+ @db.create_table(:soft_deletes_test) do
165
+ primary_key :id
166
+ time :deleted_at
167
+ end
168
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
169
+ mc.class_eval do
170
+ attr_accessor :hook_body
171
+
172
+ def around_soft_delete
173
+ super if self.hook_body.call
174
+ end
175
+ end
176
+ mc.plugin(:soft_deletes, column: :deleted_at)
177
+ @m = mc.new
178
+ end
179
+
180
+ it "has its hook called whenever an instance is soft-deleted" do
181
+ called = false
182
+ @m.hook_body = lambda do
183
+ called = true
184
+ end
185
+ @m.soft_delete
186
+ expect(@m).to be_is_soft_deleted
187
+ expect(called).to eq(true)
188
+ end
189
+
190
+ it "is not soft-deleted if its hook doesn't super" do
191
+ @m.hook_body = lambda do
192
+ false
193
+ end
194
+
195
+ expect do
196
+ @m.soft_delete
197
+ end.to raise_error(Sequel::HookFailed, /around_soft_delete hook failed/i)
198
+
199
+ expect(@m).not_to be_soft_deleted
200
+ end
201
+ end
202
+
203
+ context "extended model classes with deletion blockers" do
204
+ before do
205
+ @db.create_table(:soft_deletes_test) do
206
+ primary_key :id
207
+ time :deleted_at
208
+ end
209
+ mc = Class.new(Sequel::Model(@db[:soft_deletes_test]))
210
+ mc.class_eval do
211
+ attr_reader :stub_soft_deletion_blockers
212
+
213
+ def initialize(*)
214
+ @stub_soft_deletion_blockers = []
215
+ super
216
+ end
217
+
218
+ def soft_deletion_blockers
219
+ return self.stub_soft_deletion_blockers
220
+ end
221
+ end
222
+ mc.plugin(:soft_deletes, column: :deleted_at)
223
+ @m = mc.new
224
+ end
225
+
226
+ it "is not soft-deleted if it has deletion blockers" do
227
+ @m.stub_soft_deletion_blockers << "A BLOCKER"
228
+
229
+ expect do
230
+ @m.soft_delete
231
+ end.to raise_error(Sequel::HookFailed, /before_soft_delete hook failed/i)
232
+
233
+ expect(@m).not_to be_soft_deleted
234
+ end
235
+
236
+ it "raises an error if remove_soft_deletion_blockers hasn't been implemented" do
237
+ expect do
238
+ @m.remove_soft_deletion_blockers
239
+ end.to raise_error(NotImplementedError)
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sequel/plugins/soft_deletes"
4
+
5
+ RSpec.configure do |config|
6
+ # config.full_backtrace = true
7
+
8
+ # RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 600
9
+
10
+ config.expect_with :rspec do |expectations|
11
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
12
+ end
13
+
14
+ config.mock_with :rspec do |mocks|
15
+ mocks.verify_partial_doubles = true
16
+ end
17
+
18
+ config.order = :random
19
+ Kernel.srand config.seed
20
+
21
+ config.filter_run :focus
22
+ config.run_all_when_everything_filtered = true
23
+ config.disable_monkey_patching!
24
+ config.default_formatter = "doc" if config.files_to_run.one?
25
+ end
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel-soft-deletes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Lithic Tech
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-performance
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-sequel
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: sequel
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sqlite3
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description:
154
+ email:
155
+ - hello@lithic.tech
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - lib/sequel/plugins/soft_deletes.rb
161
+ - spec/sequel/plugins/soft_deletes_spec.rb
162
+ - spec/spec_helper.rb
163
+ homepage:
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: 2.4.0
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubygems_version: 3.1.4
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: Gem for enabling soft-deletion in tables
186
+ test_files: []