sequel-soft-deletes 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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: []