sequel_mapper 0.0.1

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.
@@ -0,0 +1,23 @@
1
+ class QueryableAssociationProxy
2
+ def initialize(database_enum, loader)
3
+ @database_enum = database_enum
4
+ @loader = loader
5
+ end
6
+
7
+ attr_reader :database_enum, :loader
8
+ private :database_enum, :loader
9
+
10
+ extend Forwardable
11
+ def_delegators :database_enum, :where
12
+
13
+ def where(criteria)
14
+ @database_enum = database_enum.where(criteria)
15
+ self
16
+ end
17
+
18
+ def each(&block)
19
+ database_enum
20
+ .map(&loader)
21
+ .each(&block)
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module SequelMapper
2
+ class StructFactory
3
+ def initialize(struct_class)
4
+ @constructor = struct_class.method(:new)
5
+ @members = struct_class.members
6
+ end
7
+
8
+ attr_reader :constructor, :members
9
+ private :constructor, :members
10
+
11
+ def call(data)
12
+ constructor.call(
13
+ *members.map { |m| data.fetch(m, nil) }
14
+ )
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module SequelMapper
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sequel_mapper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sequel_mapper"
8
+ spec.version = SequelMapper::VERSION
9
+ spec.authors = ["Stephen Best"]
10
+ spec.email = ["bestie@gmail.com"]
11
+ spec.summary = %q{A data mapper built on top of the Sequel database toolkit}
12
+ spec.description = %q{}
13
+ spec.homepage = "https://github.com/bestie/sequel_mapper"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "pry", "~> 0.10.1"
24
+ spec.add_development_dependency "rspec", "~> 3.1"
25
+ spec.add_development_dependency "pg", "~> 0.17.1"
26
+
27
+ spec.add_dependency "sequel", "~> 4.16"
28
+ end
@@ -0,0 +1,287 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper"
4
+ require "support/graph_fixture"
5
+
6
+ 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
+ }
16
+
17
+ let(:user) {
18
+ graph.where(id: "user/1").fetch(0)
19
+ }
20
+
21
+ context "without accessing associations" do
22
+ let(:modified_email) { "bestie+modified@gmail.com" }
23
+
24
+ it "saves the root object" do
25
+ user.email = modified_email
26
+ graph.save(user)
27
+
28
+ expect(datastore).to have_persisted(
29
+ :users,
30
+ hash_including(
31
+ id: "user/1",
32
+ email: modified_email,
33
+ )
34
+ )
35
+ end
36
+
37
+ it "doesn't send associated objects to the database as columns" do
38
+ user.email = modified_email
39
+ graph.save(user)
40
+
41
+ expect(datastore).not_to have_persisted(
42
+ :users,
43
+ hash_including(
44
+ posts: anything,
45
+ )
46
+ )
47
+ end
48
+ end
49
+
50
+ context "modify shallow has many associated object" do
51
+ let(:post) { user.posts.first }
52
+ let(:modified_post_body) { "modified ur body" }
53
+
54
+ it "saves the associated object" do
55
+ post.body = modified_post_body
56
+ graph.save(user)
57
+
58
+ expect(datastore).to have_persisted(
59
+ :posts,
60
+ hash_including(
61
+ id: post.id,
62
+ subject: post.subject,
63
+ author_id: post.author.id,
64
+ body: modified_post_body,
65
+ )
66
+ )
67
+ end
68
+ end
69
+
70
+ context "modify deeply nested has many associated object" do
71
+ let(:comment) {
72
+ user.posts.first.comments.to_a.last
73
+ }
74
+
75
+ let(:modified_comment_body) { "body moving, body moving" }
76
+
77
+ it "saves the associated object" do
78
+ comment.body = modified_comment_body
79
+ graph.save(user)
80
+
81
+ expect(datastore).to have_persisted(
82
+ :comments,
83
+ hash_including(
84
+ {
85
+ id: "comment/2",
86
+ post_id: "post/1",
87
+ commenter_id: "user/1",
88
+ body: modified_comment_body,
89
+ }
90
+ )
91
+ )
92
+ end
93
+ end
94
+
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
+ context "add a node to a has many assocation" do
131
+ let(:new_post_attrs) {
132
+ {
133
+ id: "posts/neu",
134
+ author: user,
135
+ subject: "I am new",
136
+ body: "new body",
137
+ comments: [],
138
+ categories: [],
139
+ }
140
+ }
141
+
142
+ let(:new_post) {
143
+ SequelMapper::StructFactory.new(
144
+ SequelMapper::GraphFixture::Post
145
+ ).call(new_post_attrs)
146
+ }
147
+
148
+ it "adds the object to the graph" do
149
+ user.posts.push(new_post)
150
+
151
+ expect(user.posts).to include(new_post)
152
+ end
153
+
154
+ it "persists the object" do
155
+ user.posts.push(new_post)
156
+ graph.save(user)
157
+
158
+ expect(datastore).to have_persisted(
159
+ :posts,
160
+ hash_including(
161
+ id: "posts/neu",
162
+ author_id: user.id,
163
+ subject: "I am new",
164
+ )
165
+ )
166
+ end
167
+ end
168
+
169
+ context "remove an object from a has many association" do
170
+ let(:post) { user.posts.first }
171
+
172
+ it "removes the object from the graph" do
173
+ user.posts.remove(post)
174
+
175
+ expect(user.posts.map(&:id)).not_to include(post.id)
176
+ end
177
+
178
+ it "removes the object from the datastore on save" do
179
+ user.posts.remove(post)
180
+ graph.save(user)
181
+
182
+ expect(datastore).not_to have_persisted(
183
+ :posts,
184
+ hash_including(
185
+ id: post.id,
186
+ )
187
+ )
188
+ end
189
+ end
190
+
191
+ context "modify a many to many relationhip" do
192
+ let(:post) { user.posts.first }
193
+
194
+ context "remove a node" do
195
+ it "mutates the graph" do
196
+ category = post.categories.first
197
+ post.categories.remove(category)
198
+
199
+ expect(post.categories.map(&:id)).not_to include(category.id)
200
+ end
201
+
202
+ it "persists the change" do
203
+ category = post.categories.first
204
+ post.categories.remove(category)
205
+ graph.save(user)
206
+
207
+ expect(datastore).not_to have_persisted(
208
+ :categories_to_posts,
209
+ {
210
+ post_id: post.id,
211
+ category_id: category.id,
212
+ }
213
+ )
214
+ end
215
+ end
216
+
217
+ context "add a node" do
218
+ let(:post_with_one_category) { user.posts.to_a.last }
219
+ let(:new_category) { user.posts.first.categories.to_a.first }
220
+
221
+ it "mutates the graph" do
222
+ post_with_one_category.categories.push(new_category)
223
+
224
+ expect(post_with_one_category.categories.map(&:id))
225
+ .to match_array(["category/1", "category/2"])
226
+ end
227
+
228
+ it "persists the change" do
229
+ post_with_one_category.categories.push(new_category)
230
+ graph.save(user)
231
+
232
+ expect(datastore).to have_persisted(
233
+ :categories_to_posts,
234
+ {
235
+ post_id: post_with_one_category.id,
236
+ category_id: new_category.id,
237
+ }
238
+ )
239
+ end
240
+ end
241
+
242
+ context "modify a node" do
243
+ let(:category) { user.posts.first.categories.first }
244
+ let(:modified_category_name) { "modified category" }
245
+
246
+ it "mutates the graph" do
247
+ category.name = modified_category_name
248
+
249
+ expect(post.categories.first.name)
250
+ .to eq(modified_category_name)
251
+ end
252
+
253
+ it "persists the change" do
254
+ category.name = modified_category_name
255
+ graph.save(user)
256
+
257
+ expect(datastore).to have_persisted(
258
+ :categories,
259
+ {
260
+ id: category.id,
261
+ name: modified_category_name,
262
+ }
263
+ )
264
+ end
265
+ end
266
+ end
267
+
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
+ }
277
+ end
278
+
279
+ failure_message do |datastore|
280
+ "expected #{datastore[relation_name]} to have persisted #{data.inspect} in #{relation_name}"
281
+ end
282
+
283
+ failure_message_when_negated do |datastore|
284
+ failure_message.gsub("to have", "not to have")
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,85 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper"
4
+ require "support/graph_fixture"
5
+
6
+ RSpec.describe "Graph traversal" do
7
+ include SequelMapper::GraphFixture
8
+
9
+ describe "assocaitions" do
10
+ subject(:graph) {
11
+ SequelMapper::Graph.new(
12
+ top_level_namespace: :users,
13
+ datastore: datastore,
14
+ relation_mappings: relation_mappings,
15
+ )
16
+ }
17
+
18
+ let(:user_query) {
19
+ graph.where(id: "user/1")
20
+ }
21
+
22
+ it "finds data via the storage adapter" do
23
+ expect(user_query.count).to be 1
24
+ end
25
+
26
+ it "maps the raw data from the store into domain objects" do
27
+ expect(user_query.first.id).to eq("user/1")
28
+ expect(user_query.first.first_name).to eq("Stephen")
29
+ end
30
+
31
+ it "handles has_many associations" do
32
+ expect(user_query.first.posts.first.subject)
33
+ .to eq("Object mapping")
34
+ end
35
+
36
+ it "handles nested has_many associations" do
37
+ expect(
38
+ user_query.first
39
+ .posts.first
40
+ .comments.first
41
+ .body
42
+ ).to eq("Trololol")
43
+ end
44
+
45
+ describe "lazy loading" do
46
+ let(:post_factory) { double(:post_factory, call: nil) }
47
+
48
+ it "loads has many associations lazily" do
49
+ posts = user_query.first.posts
50
+
51
+ expect(post_factory).not_to have_received(:call)
52
+ end
53
+ end
54
+
55
+ it "maps belongs to assocations" do
56
+ expect(user_query.first.posts.first.author.id)
57
+ .to eq("user/1")
58
+ end
59
+
60
+ describe "identity map" do
61
+ it "always returns (a proxy of) the same object for a given id" do
62
+ expect(user_query.first.posts.first.author.__getobj__)
63
+ .to be(user_query.first)
64
+ end
65
+ end
66
+
67
+ it "maps deeply nested belongs to assocations" do
68
+ expect(user_query.first.posts.first.comments.first.commenter.id)
69
+ .to eq("user/2")
70
+ end
71
+
72
+ it "maps has many to many associations as has many through" do
73
+ expect(user_query.first.posts.first.categories.map(&:id))
74
+ .to match_array(["category/1", "category/2"])
75
+
76
+ expect(user_query.first.posts.first.categories.to_a.last.posts.map(&:id))
77
+ .to match_array(["post/1", "post/2"])
78
+ end
79
+
80
+ xit "combines has many through associations" do
81
+ expect(user_query.first.categories_posted_in.map(&:id))
82
+ .to match_array(["category/1", "category/2"])
83
+ end
84
+ end
85
+ end