sequel_mapper 0.0.1 → 0.0.3

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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CODE_OF_CONDUCT.md +28 -0
  4. data/Gemfile.lock +32 -2
  5. data/MissingFeatures.md +64 -0
  6. data/README.md +141 -72
  7. data/Rakefile +29 -0
  8. data/TODO.md +16 -11
  9. data/features/env.rb +57 -0
  10. data/features/example.feature +121 -0
  11. data/features/step_definitions/example_steps.rb +46 -0
  12. data/lib/sequel_mapper.rb +6 -2
  13. data/lib/sequel_mapper/abstract_record.rb +53 -0
  14. data/lib/sequel_mapper/association_loaders.rb +52 -0
  15. data/lib/sequel_mapper/collection_mutability_proxy.rb +77 -0
  16. data/lib/sequel_mapper/configurations/conventional_association_configuration.rb +187 -0
  17. data/lib/sequel_mapper/configurations/conventional_configuration.rb +269 -0
  18. data/lib/sequel_mapper/dataset.rb +37 -0
  19. data/lib/sequel_mapper/deleted_record.rb +16 -0
  20. data/lib/sequel_mapper/dirty_map.rb +31 -0
  21. data/lib/sequel_mapper/graph_loader.rb +48 -0
  22. data/lib/sequel_mapper/graph_serializer.rb +107 -0
  23. data/lib/sequel_mapper/identity_map.rb +22 -0
  24. data/lib/sequel_mapper/lazy_object_proxy.rb +51 -0
  25. data/lib/sequel_mapper/many_to_many_association.rb +181 -0
  26. data/lib/sequel_mapper/many_to_one_association.rb +60 -0
  27. data/lib/sequel_mapper/mapper_facade.rb +180 -0
  28. data/lib/sequel_mapper/one_to_many_association.rb +51 -0
  29. data/lib/sequel_mapper/public_conveniencies.rb +27 -0
  30. data/lib/sequel_mapper/query_order.rb +32 -0
  31. data/lib/sequel_mapper/queryable_lazy_dataset_loader.rb +70 -0
  32. data/lib/sequel_mapper/relation_mapping.rb +35 -0
  33. data/lib/sequel_mapper/serializer.rb +18 -0
  34. data/lib/sequel_mapper/short_inspection_string.rb +18 -0
  35. data/lib/sequel_mapper/subset_queries_proxy.rb +11 -0
  36. data/lib/sequel_mapper/upserted_record.rb +15 -0
  37. data/lib/sequel_mapper/version.rb +1 -1
  38. data/sequel_mapper.gemspec +3 -0
  39. data/spec/config_override_spec.rb +167 -0
  40. data/spec/custom_serializers_spec.rb +77 -0
  41. data/spec/deletion_spec.rb +104 -0
  42. data/spec/graph_persistence_spec.rb +83 -88
  43. data/spec/graph_traversal_spec.rb +32 -31
  44. data/spec/new_graph_persistence_spec.rb +69 -0
  45. data/spec/object_identity_spec.rb +70 -0
  46. data/spec/ordered_association_spec.rb +46 -16
  47. data/spec/persistence_efficiency_spec.rb +186 -0
  48. data/spec/predefined_queries_spec.rb +73 -0
  49. data/spec/proxying_spec.rb +25 -19
  50. data/spec/querying_spec.rb +24 -27
  51. data/spec/readme_examples_spec.rb +35 -0
  52. data/spec/sequel_mapper/abstract_record_spec.rb +179 -0
  53. data/spec/sequel_mapper/{association_proxy_spec.rb → collection_mutability_proxy_spec.rb} +6 -6
  54. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  55. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  56. data/spec/sequel_mapper/public_conveniencies_spec.rb +49 -0
  57. data/spec/sequel_mapper/queryable_lazy_dataset_loader_spec.rb +103 -0
  58. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  59. data/spec/spec_helper.rb +7 -10
  60. data/spec/support/blog_schema.rb +29 -0
  61. data/spec/support/have_persisted_matcher.rb +19 -0
  62. data/spec/support/mapper_setup.rb +234 -0
  63. data/spec/support/mock_sequel.rb +0 -1
  64. data/spec/support/object_graph_setup.rb +106 -0
  65. data/spec/support/seed_data_setup.rb +122 -0
  66. data/spec/support/sequel_persistence_setup.rb +19 -0
  67. data/spec/support/sequel_test_support.rb +159 -0
  68. metadata +121 -15
  69. data/lib/sequel_mapper/association_proxy.rb +0 -54
  70. data/lib/sequel_mapper/belongs_to_association_proxy.rb +0 -27
  71. data/lib/sequel_mapper/graph.rb +0 -174
  72. data/lib/sequel_mapper/queryable_association_proxy.rb +0 -23
  73. data/spec/sequel_mapper/belongs_to_association_proxy_spec.rb +0 -65
  74. data/spec/support/graph_fixture.rb +0 -331
  75. data/spec/support/query_counter.rb +0 -29
@@ -0,0 +1,18 @@
1
+ module SequelMapper
2
+ class Serializer
3
+ def initialize(field_names, object)
4
+ @field_names = field_names
5
+ @object = object
6
+ end
7
+
8
+ attr_reader :field_names, :object
9
+
10
+ def to_h
11
+ Hash[
12
+ field_names.map { |field_name|
13
+ [field_name, object.public_send(field_name)]
14
+ }
15
+ ]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module SequelMapper
2
+ module ShortInspectionString
3
+ def inspect
4
+ "\#<#{self.class.name}:#{self.object_id.<<(1).to_s(16)} " +
5
+ inspectable_properties.map { |property|
6
+ [
7
+ property,
8
+ instance_variable_get("@#{property}").inspect
9
+ ].join("=")
10
+ }
11
+ .join(" ") + ">"
12
+ end
13
+
14
+ def inspectable_properties
15
+ []
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module SequelMapper
2
+ class SubsetQueriesProxy
3
+ def initialize(query_map)
4
+ @query_map = query_map
5
+ end
6
+
7
+ def execute(superset, name, *params)
8
+ @query_map.fetch(name).call(superset, *params)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ require "sequel_mapper/abstract_record"
2
+
3
+ module SequelMapper
4
+ class UpsertedRecord < AbstractRecord
5
+ def if_upsert(&block)
6
+ block.call(self)
7
+ self
8
+ end
9
+
10
+ protected
11
+ def operation
12
+ :upsert
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module SequelMapper
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -22,7 +22,10 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
23
  spec.add_development_dependency "pry", "~> 0.10.1"
24
24
  spec.add_development_dependency "rspec", "~> 3.1"
25
+ spec.add_development_dependency "cucumber"
25
26
  spec.add_development_dependency "pg", "~> 0.17.1"
26
27
 
27
28
  spec.add_dependency "sequel", "~> 4.16"
29
+ spec.add_dependency "activesupport", "~> 4.0"
30
+ spec.add_dependency "fetchable", "~> 1.0"
28
31
  end
@@ -0,0 +1,167 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "sequel_mapper"
7
+
8
+ require "sequel_mapper/configurations/conventional_configuration"
9
+
10
+ RSpec.describe "Configuration override" do
11
+ include_context "mapper setup"
12
+ include_context "sequel persistence setup"
13
+ include_context "seed data setup"
14
+
15
+ subject(:user_mapper) {
16
+ SequelMapper.mapper(
17
+ config: mapper_config,
18
+ name: :users,
19
+ datastore: datastore,
20
+ )
21
+ }
22
+
23
+ let(:mapper_config) {
24
+ SequelMapper::Configurations::ConventionalConfiguration.new(datastore)
25
+ .setup_mapping(:users) { |users|
26
+ users.has_many :posts, foreign_key: :author_id
27
+ }
28
+ }
29
+
30
+ let(:user) {
31
+ user_mapper.where(id: "users/1").first
32
+ }
33
+
34
+ context "override the root mapper factory" do
35
+ context "with a Struct class" do
36
+ before do
37
+ mapper_config.setup_mapping(:users) do |config|
38
+ config.class(user_subclass)
39
+ end
40
+ end
41
+
42
+ let(:user_subclass) { Class.new(User) }
43
+
44
+ it "uses the class from the override" do
45
+ expect(user.class).to be(user_subclass)
46
+ end
47
+ end
48
+ end
49
+
50
+ context "override an association" do
51
+ context "with a callable factory" do
52
+ before do
53
+ mapper_config.setup_mapping(:posts) do |config|
54
+ config.factory(override_post_factory)
55
+ end
56
+ end
57
+
58
+ let(:post_subclass) { Class.new(Post) }
59
+
60
+ let(:override_post_factory) {
61
+ SequelMapper::StructFactory.new(post_subclass)
62
+ }
63
+
64
+ let(:posts) {
65
+ user.posts
66
+ }
67
+
68
+ it "uses the specified factory" do
69
+ expect(posts.first.class).to be(post_subclass)
70
+ end
71
+ end
72
+ end
73
+
74
+ context "override table names" do
75
+ context "for just the top level mapping" do
76
+ before do
77
+ datastore.rename_table(:users, unconventional_table_name)
78
+ end
79
+
80
+ after do
81
+ datastore.rename_table(unconventional_table_name, :users)
82
+ end
83
+
84
+ let(:mapper_config) {
85
+ SequelMapper::Configurations::ConventionalConfiguration
86
+ .new(datastore)
87
+ .setup_mapping(:users) do |config|
88
+ config.relation_name unconventional_table_name
89
+ end
90
+ }
91
+
92
+
93
+ let(:datastore) { db_connection }
94
+
95
+ let(:unconventional_table_name) {
96
+ :users_is_called_this_weird_thing_perhaps_for_legacy_reasons
97
+ }
98
+
99
+ it "maps data from the specified relation" do
100
+ expect(
101
+ user_mapper.map(&:id)
102
+ ).to eq(["users/1", "users/2", "users/3"])
103
+ end
104
+ end
105
+
106
+ context "for associated collections" do
107
+ before do
108
+ rename_all_the_tables
109
+ setup_the_strange_table_name_mappings
110
+ end
111
+
112
+ after do
113
+ undo_rename_all_the_tables
114
+ end
115
+
116
+ def rename_all_the_tables
117
+ strange_table_name_map.each do |name, new_name|
118
+ datastore.rename_table(name, new_name)
119
+ end
120
+ end
121
+
122
+ def undo_rename_all_the_tables
123
+ strange_table_name_map.each do |original_name, strange_name|
124
+ datastore.rename_table(strange_name, original_name)
125
+ end
126
+ end
127
+
128
+ def setup_the_strange_table_name_mappings
129
+ mapper_config
130
+ .setup_mapping(:users) do |config|
131
+ config.relation_name strange_table_name_map.fetch(:users)
132
+ config.has_many(:posts, foreign_key: :author_id)
133
+ end
134
+ .setup_mapping(:posts) do |config|
135
+ config.relation_name strange_table_name_map.fetch(:posts)
136
+ config.belongs_to(:author, mapping_name: :users)
137
+ config.has_many_through(:categories, through_mapping_name: strange_table_name_map.fetch(:categories_to_posts))
138
+ end
139
+ .setup_mapping(:categories) do |config|
140
+ config.relation_name strange_table_name_map.fetch(:categories)
141
+ config.has_many_through(:posts, through_mapping_name: strange_table_name_map.fetch(:categories_to_posts))
142
+ end
143
+ end
144
+
145
+ let(:strange_table_name_map) {
146
+ {
147
+ :users => :users_table_that_has_silly_name_perhaps_for_legacy_reasons,
148
+ :posts => :thank_you_past_self_for_this_excellent_name,
149
+ :categories => :these_are_the_categories_for_real,
150
+ :categories_to_posts => :this_one_is_just_full_of_bees,
151
+ }
152
+ }
153
+
154
+ it "maps data from the specified relation into a has many collection" do
155
+ expect(
156
+ user.posts.map(&:id)
157
+ ).to eq(["posts/1", "posts/2"])
158
+ end
159
+
160
+ it "maps data from the specified relation into a `belongs_to` field" do
161
+ expect(
162
+ user.posts.first.author.__getobj__.object_id
163
+ ).to eq(user.object_id)
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,77 @@
1
+ require "spec_helper"
2
+
3
+ require "support/have_persisted_matcher"
4
+ require "support/mapper_setup"
5
+ require "support/sequel_persistence_setup"
6
+ require "support/seed_data_setup"
7
+ require "sequel_mapper"
8
+
9
+ require "sequel_mapper/configurations/conventional_configuration"
10
+
11
+ RSpec.describe "Config override" do
12
+ include_context "mapper setup"
13
+ include_context "sequel persistence setup"
14
+ include_context "seed data setup"
15
+
16
+
17
+ subject(:user_mapper) {
18
+ SequelMapper.mapper(
19
+ config: mapper_config,
20
+ name: :users,
21
+ datastore: datastore,
22
+ )
23
+ }
24
+
25
+ let(:mapper_config) {
26
+ SequelMapper::Configurations::ConventionalConfiguration.new(datastore)
27
+ .setup_mapping(:users) { |users|
28
+ users.has_many :posts, foreign_key: :author_id
29
+ }
30
+ }
31
+
32
+ let(:user) { user_mapper.where(id: "users/1").first }
33
+
34
+ context "with an object that has private fields" do
35
+ let(:user_class) {
36
+ Class.new(User) {
37
+ private :first_name
38
+ private :last_name
39
+
40
+ def full_name
41
+ [first_name, last_name].join(" ")
42
+ end
43
+ }
44
+ }
45
+
46
+ let(:user_serializer) {
47
+ ->(object) {
48
+ object.to_h.merge(
49
+ first_name: "I am a custom serializer",
50
+ last_name: "and i don't care about facts",
51
+ )
52
+ }
53
+ }
54
+
55
+ before do
56
+ mapper_config.setup_mapping(:users) do |config|
57
+ config.class(user_class)
58
+ config.serializer(user_serializer)
59
+ end
60
+ end
61
+
62
+ context "when saving the object" do
63
+ it "uses the custom serializer" do
64
+ user.first_name = "This won't work"
65
+ user.last_name = "because the serialzer is weird"
66
+
67
+ user_mapper.save(user)
68
+
69
+ expect(datastore).to have_persisted(:users, hash_including(
70
+ id: user.id,
71
+ first_name: "I am a custom serializer",
72
+ last_name: "and i don't care about facts",
73
+ ))
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,104 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "support/have_persisted_matcher"
7
+ require "sequel_mapper"
8
+
9
+ RSpec.describe "Deletion" do
10
+ include_context "mapper setup"
11
+ include_context "sequel persistence setup"
12
+ include_context "seed data setup"
13
+
14
+ subject(:mapper) { user_mapper }
15
+
16
+ let(:user) {
17
+ mapper.where(id: "users/1").first
18
+ }
19
+
20
+ let(:reloaded_user) {
21
+ mapper.where(id: "users/1").first
22
+ }
23
+
24
+ describe "Deleting the root" do
25
+ it "deletes the root object" do
26
+ mapper.delete(user)
27
+
28
+ expect(datastore).not_to have_persisted(
29
+ :users,
30
+ hash_including(id: "users/1")
31
+ )
32
+ end
33
+
34
+ context "when much of the graph has been loaded" do
35
+ before do
36
+ user.posts.flat_map(&:comments)
37
+ end
38
+
39
+ it "deletes the root object" do
40
+ mapper.delete(user)
41
+
42
+ expect(datastore).not_to have_persisted(
43
+ :users,
44
+ hash_including(id: "users/1")
45
+ )
46
+ end
47
+
48
+ it "does not delete the child objects" do
49
+ expect {
50
+ mapper.delete(user)
51
+ }.not_to change { [datastore[:posts], datastore[:comments]].map(&:count) }
52
+ end
53
+ end
54
+
55
+ # context "deleting multiple" do
56
+ # it "is not currently supported"
57
+ # end
58
+ end
59
+
60
+ describe "Deleting a child object (one to many)" do
61
+ let(:post) {
62
+ user.posts.find { |post| post.id == "posts/1" }
63
+ }
64
+
65
+ it "deletes the specified node" do
66
+ user.posts.delete(post)
67
+ mapper.save(user)
68
+
69
+ expect(datastore).not_to have_persisted(
70
+ :posts,
71
+ hash_including(id: "posts/1")
72
+ )
73
+ end
74
+
75
+ it "does not delete the parent object" do
76
+ user.posts.delete(post)
77
+ mapper.save(user)
78
+
79
+ expect(datastore).to have_persisted(
80
+ :users,
81
+ hash_including(id: "users/1")
82
+ )
83
+ end
84
+
85
+ it "does not delete the sibling objects" do
86
+ user.posts.delete(post)
87
+ mapper.save(user)
88
+
89
+ expect(reloaded_user.posts.count).to be > 0
90
+ end
91
+
92
+ it "does not cascade delete" do
93
+ user.posts.delete(post)
94
+ mapper.save(user)
95
+
96
+ expect(datastore).to have_persisted(
97
+ :comments,
98
+ hash_including(
99
+ post_id: "posts/1",
100
+ )
101
+ )
102
+ end
103
+ end
104
+ end
@@ -1,34 +1,33 @@
1
1
  require "spec_helper"
2
2
 
3
+ require "support/have_persisted_matcher"
4
+ require "support/mapper_setup"
5
+ require "support/sequel_persistence_setup"
6
+ require "support/seed_data_setup"
3
7
  require "sequel_mapper"
4
- require "support/graph_fixture"
5
8
 
6
9
  RSpec.describe "Graph persistence" do
7
- include SequelMapper::GraphFixture
8
-
9
- subject(:graph) {
10
- SequelMapper::Graph.new(
11
- top_level_namespace: :users,
12
- datastore: datastore,
13
- relation_mappings: relation_mappings,
14
- )
15
- }
10
+ include_context "mapper setup"
11
+ include_context "sequel persistence setup"
12
+ include_context "seed data setup"
13
+
14
+ subject(:mapper) { mappers.fetch(:users) }
16
15
 
17
16
  let(:user) {
18
- graph.where(id: "user/1").fetch(0)
17
+ mapper.where(id: "users/1").first
19
18
  }
20
19
 
21
- context "without accessing associations" do
20
+ context "without associations" do
22
21
  let(:modified_email) { "bestie+modified@gmail.com" }
23
22
 
24
23
  it "saves the root object" do
25
24
  user.email = modified_email
26
- graph.save(user)
25
+ mapper.save(user)
27
26
 
28
27
  expect(datastore).to have_persisted(
29
28
  :users,
30
29
  hash_including(
31
- id: "user/1",
30
+ id: "users/1",
32
31
  email: modified_email,
33
32
  )
34
33
  )
@@ -36,7 +35,7 @@ RSpec.describe "Graph persistence" do
36
35
 
37
36
  it "doesn't send associated objects to the database as columns" do
38
37
  user.email = modified_email
39
- graph.save(user)
38
+ mapper.save(user)
40
39
 
41
40
  expect(datastore).not_to have_persisted(
42
41
  :users,
@@ -45,6 +44,23 @@ RSpec.describe "Graph persistence" do
45
44
  )
46
45
  )
47
46
  end
47
+
48
+ # TODO move to a dirty tracking spec?
49
+ context "when mutating entity fields in place" do
50
+ it "saves the object" do
51
+ user.email << "MUTATED"
52
+
53
+ mapper.save(user)
54
+
55
+ expect(datastore).to have_persisted(
56
+ :users,
57
+ hash_including(
58
+ id: "users/1",
59
+ email: /MUTATED$/,
60
+ )
61
+ )
62
+ end
63
+ end
48
64
  end
49
65
 
50
66
  context "modify shallow has many associated object" do
@@ -53,14 +69,14 @@ RSpec.describe "Graph persistence" do
53
69
 
54
70
  it "saves the associated object" do
55
71
  post.body = modified_post_body
56
- graph.save(user)
72
+ mapper.save(user)
57
73
 
58
74
  expect(datastore).to have_persisted(
59
75
  :posts,
60
76
  hash_including(
61
77
  id: post.id,
62
78
  subject: post.subject,
63
- author_id: post.author.id,
79
+ author_id: user.id,
64
80
  body: modified_post_body,
65
81
  )
66
82
  )
@@ -69,22 +85,22 @@ RSpec.describe "Graph persistence" do
69
85
 
70
86
  context "modify deeply nested has many associated object" do
71
87
  let(:comment) {
72
- user.posts.first.comments.to_a.last
88
+ user.posts.first.comments.first
73
89
  }
74
90
 
75
91
  let(:modified_comment_body) { "body moving, body moving" }
76
92
 
77
93
  it "saves the associated object" do
78
94
  comment.body = modified_comment_body
79
- graph.save(user)
95
+ mapper.save(user)
80
96
 
81
97
  expect(datastore).to have_persisted(
82
98
  :comments,
83
99
  hash_including(
84
100
  {
85
- id: "comment/2",
86
- post_id: "post/1",
87
- commenter_id: "user/1",
101
+ id: "comments/1",
102
+ post_id: "posts/1",
103
+ commenter_id: "users/1",
88
104
  body: modified_comment_body,
89
105
  }
90
106
  )
@@ -92,41 +108,6 @@ RSpec.describe "Graph persistence" do
92
108
  end
93
109
  end
94
110
 
95
- context "modify the foreign_key of an object" do
96
- let(:original_author) { user }
97
- let(:new_author) { graph.where(id: "user/2").first }
98
- let(:post) { original_author.posts.first }
99
-
100
- it "persists the change in ownership" do
101
- post.author = new_author
102
- graph.save(user)
103
-
104
- expect(datastore).to have_persisted(
105
- :posts,
106
- hash_including(
107
- id: post.id,
108
- author_id: new_author.id,
109
- )
110
- )
111
- end
112
-
113
- it "removes the object form the original graph" do
114
- post.author = new_author
115
- graph.save(user)
116
-
117
- expect(original_author.posts.to_a.map(&:id))
118
- .not_to include("posts/1")
119
- end
120
-
121
- it "adds the object to the appropriate graph" do
122
- post.author = new_author
123
- graph.save(user)
124
-
125
- expect(new_author.posts.to_a.map(&:id))
126
- .to include("post/1")
127
- end
128
- end
129
-
130
111
  context "add a node to a has many assocation" do
131
112
  let(:new_post_attrs) {
132
113
  {
@@ -141,7 +122,7 @@ RSpec.describe "Graph persistence" do
141
122
 
142
123
  let(:new_post) {
143
124
  SequelMapper::StructFactory.new(
144
- SequelMapper::GraphFixture::Post
125
+ Post
145
126
  ).call(new_post_attrs)
146
127
  }
147
128
 
@@ -153,7 +134,8 @@ RSpec.describe "Graph persistence" do
153
134
 
154
135
  it "persists the object" do
155
136
  user.posts.push(new_post)
156
- graph.save(user)
137
+
138
+ mapper.save(user)
157
139
 
158
140
  expect(datastore).to have_persisted(
159
141
  :posts,
@@ -166,18 +148,18 @@ RSpec.describe "Graph persistence" do
166
148
  end
167
149
  end
168
150
 
169
- context "remove an object from a has many association" do
151
+ context "delete an object from a has many association" do
170
152
  let(:post) { user.posts.first }
171
153
 
172
- it "removes the object from the graph" do
173
- user.posts.remove(post)
154
+ it "delete the object from the graph" do
155
+ user.posts.delete(post)
174
156
 
175
157
  expect(user.posts.map(&:id)).not_to include(post.id)
176
158
  end
177
159
 
178
- it "removes the object from the datastore on save" do
179
- user.posts.remove(post)
180
- graph.save(user)
160
+ it "delete the object from the datastore on save" do
161
+ user.posts.delete(post)
162
+ mapper.save(user)
181
163
 
182
164
  expect(datastore).not_to have_persisted(
183
165
  :posts,
@@ -188,21 +170,21 @@ RSpec.describe "Graph persistence" do
188
170
  end
189
171
  end
190
172
 
191
- context "modify a many to many relationhip" do
173
+ context "modify a many to many relationship" do
192
174
  let(:post) { user.posts.first }
193
175
 
194
- context "remove a node" do
176
+ context "delete a node" do
195
177
  it "mutates the graph" do
196
178
  category = post.categories.first
197
- post.categories.remove(category)
179
+ post.categories.delete(category)
198
180
 
199
181
  expect(post.categories.map(&:id)).not_to include(category.id)
200
182
  end
201
183
 
202
- it "persists the change" do
184
+ it "deletes the 'join table' record" do
203
185
  category = post.categories.first
204
- post.categories.remove(category)
205
- graph.save(user)
186
+ post.categories.delete(category)
187
+ mapper.save(user)
206
188
 
207
189
  expect(datastore).not_to have_persisted(
208
190
  :categories_to_posts,
@@ -212,6 +194,19 @@ RSpec.describe "Graph persistence" do
212
194
  }
213
195
  )
214
196
  end
197
+
198
+ it "does not delete the object" do
199
+ category = post.categories.first
200
+ post.categories.delete(category)
201
+ mapper.save(user)
202
+
203
+ expect(datastore).to have_persisted(
204
+ :categories,
205
+ hash_including(
206
+ id: category.id,
207
+ )
208
+ )
209
+ end
215
210
  end
216
211
 
217
212
  context "add a node" do
@@ -222,12 +217,12 @@ RSpec.describe "Graph persistence" do
222
217
  post_with_one_category.categories.push(new_category)
223
218
 
224
219
  expect(post_with_one_category.categories.map(&:id))
225
- .to match_array(["category/1", "category/2"])
220
+ .to match_array(["categories/1", "categories/2"])
226
221
  end
227
222
 
228
223
  it "persists the change" do
229
224
  post_with_one_category.categories.push(new_category)
230
- graph.save(user)
225
+ mapper.save(user)
231
226
 
232
227
  expect(datastore).to have_persisted(
233
228
  :categories_to_posts,
@@ -252,7 +247,7 @@ RSpec.describe "Graph persistence" do
252
247
 
253
248
  it "persists the change" do
254
249
  category.name = modified_category_name
255
- graph.save(user)
250
+ mapper.save(user)
256
251
 
257
252
  expect(datastore).to have_persisted(
258
253
  :categories,
@@ -265,23 +260,23 @@ RSpec.describe "Graph persistence" do
265
260
  end
266
261
  end
267
262
 
268
- RSpec::Matchers.define :have_persisted do |relation_name, data|
269
- match do |datastore|
270
- datastore[relation_name].find { |record|
271
- if data.respond_to?(:===)
272
- data === record
273
- else
274
- data == record
275
- end
276
- }
263
+ context "when a save operation fails (some object is not persistable)" do
264
+ before do
265
+ user.posts.first.subject = "UNRELATED CHANGE THAT WILL FAIL"
266
+ user.email = unpersistable_object
277
267
  end
278
268
 
279
- failure_message do |datastore|
280
- "expected #{datastore[relation_name]} to have persisted #{data.inspect} in #{relation_name}"
281
- end
269
+ let(:unpersistable_object) { ->() { } }
270
+
271
+ it "rolls back the transation" do
272
+ begin
273
+ mapper.save(user)
274
+ rescue Sequel::Error
275
+ end
282
276
 
283
- failure_message_when_negated do |datastore|
284
- failure_message.gsub("to have", "not to have")
277
+ expect(datastore).not_to have_persisted(:posts, hash_including(
278
+ subject: "UNRELATED CHANGE THAT WILL FAIL"
279
+ ))
285
280
  end
286
281
  end
287
282
  end