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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +43 -0
- data/LICENSE.txt +22 -0
- data/README.md +112 -0
- data/Rakefile +2 -0
- data/TODO.md +33 -0
- data/lib/sequel_mapper.rb +4 -0
- data/lib/sequel_mapper/association_proxy.rb +54 -0
- data/lib/sequel_mapper/belongs_to_association_proxy.rb +27 -0
- data/lib/sequel_mapper/graph.rb +174 -0
- data/lib/sequel_mapper/queryable_association_proxy.rb +23 -0
- data/lib/sequel_mapper/struct_factory.rb +17 -0
- data/lib/sequel_mapper/version.rb +3 -0
- data/sequel_mapper.gemspec +28 -0
- data/spec/graph_persistence_spec.rb +287 -0
- data/spec/graph_traversal_spec.rb +85 -0
- data/spec/ordered_association_spec.rb +29 -0
- data/spec/proxying_spec.rb +82 -0
- data/spec/querying_spec.rb +51 -0
- data/spec/sequel_mapper/association_proxy_spec.rb +95 -0
- data/spec/sequel_mapper/belongs_to_association_proxy_spec.rb +65 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/graph_fixture.rb +331 -0
- data/spec/support/mock_sequel.rb +194 -0
- data/spec/support/query_counter.rb +29 -0
- metadata +167 -0
@@ -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,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
|