sequel_mapper 0.0.1 → 0.0.3

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