rom-repository 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -5
- data/CHANGELOG.md +24 -0
- data/Gemfile +21 -5
- data/README.md +6 -110
- data/lib/rom/repository/changeset/create.rb +26 -0
- data/lib/rom/repository/changeset/pipe.rb +40 -0
- data/lib/rom/repository/changeset/update.rb +82 -0
- data/lib/rom/repository/changeset.rb +99 -0
- data/lib/rom/repository/class_interface.rb +142 -0
- data/lib/rom/repository/command_compiler.rb +214 -0
- data/lib/rom/repository/command_proxy.rb +22 -0
- data/lib/rom/repository/header_builder.rb +13 -16
- data/lib/rom/repository/mapper_builder.rb +7 -14
- data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
- data/lib/rom/repository/relation_proxy.rb +225 -0
- data/lib/rom/repository/root.rb +110 -0
- data/lib/rom/repository/struct_attributes.rb +46 -0
- data/lib/rom/repository/struct_builder.rb +31 -14
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/repository.rb +192 -31
- data/lib/rom/struct.rb +13 -8
- data/rom-repository.gemspec +9 -10
- data/spec/integration/changeset_spec.rb +86 -0
- data/spec/integration/command_macros_spec.rb +175 -0
- data/spec/integration/command_spec.rb +224 -0
- data/spec/integration/multi_adapter_spec.rb +3 -3
- data/spec/integration/repository_spec.rb +97 -2
- data/spec/integration/root_repository_spec.rb +88 -0
- data/spec/shared/database.rb +47 -3
- data/spec/shared/mappers.rb +35 -0
- data/spec/shared/models.rb +41 -0
- data/spec/shared/plugins.rb +66 -0
- data/spec/shared/relations.rb +76 -0
- data/spec/shared/repo.rb +38 -17
- data/spec/shared/seeds.rb +19 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/support/mapper_registry.rb +1 -3
- data/spec/unit/changeset_spec.rb +58 -0
- data/spec/unit/header_builder_spec.rb +34 -35
- data/spec/unit/relation_proxy_spec.rb +170 -0
- data/spec/unit/sql/relation_spec.rb +5 -5
- data/spec/unit/struct_builder_spec.rb +7 -4
- data/spec/unit/struct_spec.rb +22 -0
- metadata +38 -41
- data/lib/rom/plugins/relation/key_inference.rb +0 -31
- data/lib/rom/repository/loading_proxy/combine.rb +0 -158
- data/lib/rom/repository/loading_proxy.rb +0 -182
- data/spec/unit/loading_proxy_spec.rb +0 -147
@@ -0,0 +1,175 @@
|
|
1
|
+
RSpec.describe ROM::Repository, '.command' do
|
2
|
+
include_context 'database'
|
3
|
+
include_context 'relations'
|
4
|
+
|
5
|
+
it 'allows configuring a create command' do
|
6
|
+
repo = Class.new(ROM::Repository[:users]) do
|
7
|
+
commands :create
|
8
|
+
end.new(rom)
|
9
|
+
|
10
|
+
user = repo.create(name: 'Jane')
|
11
|
+
|
12
|
+
expect(user.id).to_not be(nil)
|
13
|
+
expect(user.name).to eql('Jane')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'allows configuring an update and delete commands' do
|
17
|
+
repo = Class.new(ROM::Repository[:users]) do
|
18
|
+
commands :create, update: :by_pk, delete: :by_pk
|
19
|
+
end.new(rom)
|
20
|
+
|
21
|
+
user = repo.create(name: 'Jane')
|
22
|
+
|
23
|
+
repo.update(user.id, name: 'Jane Doe')
|
24
|
+
|
25
|
+
user = repo.users.by_pk(user.id).one
|
26
|
+
|
27
|
+
expect(user.name).to eql('Jane Doe')
|
28
|
+
|
29
|
+
repo.delete(user.id)
|
30
|
+
|
31
|
+
expect(repo.users.by_pk(user.id).one).to be(nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'configures an update command with custom restriction' do
|
35
|
+
repo = Class.new(ROM::Repository[:users]) do
|
36
|
+
commands update: :by_name
|
37
|
+
end.new(rom)
|
38
|
+
|
39
|
+
repo.relations.users.insert(name: 'Jade')
|
40
|
+
|
41
|
+
user = repo.update('Jade', name: 'Jade Doe')
|
42
|
+
expect(user.name).to eql('Jade Doe')
|
43
|
+
|
44
|
+
expect(repo.update('Oops', name: 'Jade')).to be(nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'allows to configure update command without create one' do
|
48
|
+
repo = Class.new(ROM::Repository[:users]) do
|
49
|
+
commands update: :by_pk
|
50
|
+
end.new(rom)
|
51
|
+
|
52
|
+
user = repo.command(create: :users)[name: 'Jane']
|
53
|
+
|
54
|
+
repo.update(user.id, name: 'Jane Doe')
|
55
|
+
|
56
|
+
updated_user = repo.users.by_pk(user.id).one
|
57
|
+
|
58
|
+
expect(updated_user.name).to eql('Jane Doe')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'allows defining a single command with multiple views' do
|
62
|
+
repo = Class.new(ROM::Repository[:users]) do
|
63
|
+
commands :create, update: [:by_pk, :by_name]
|
64
|
+
end.new(rom)
|
65
|
+
|
66
|
+
user = repo.create(name: 'Jane')
|
67
|
+
|
68
|
+
repo.update_by_pk(user.id, name: 'Jane Doe')
|
69
|
+
user = repo.users.by_pk(user.id).one
|
70
|
+
expect(user.name).to eql('Jane Doe')
|
71
|
+
|
72
|
+
repo.update_by_name(user.name, name: 'Jane')
|
73
|
+
user = repo.users.by_pk(user.id).one
|
74
|
+
expect(user.name).to eql('Jane')
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'uses a mapper built from AST by default' do
|
78
|
+
repo = Class.new(ROM::Repository[:users]) do
|
79
|
+
commands :create
|
80
|
+
end.new(rom)
|
81
|
+
|
82
|
+
user = repo.create(name: 'Jane')
|
83
|
+
|
84
|
+
expect(user).to be_kind_of ROM::Struct
|
85
|
+
|
86
|
+
struct_definition = [:users, [:header, [[:attribute, :id], [:attribute, :name]]]]
|
87
|
+
expect(user).to be_an_instance_of ROM::Repository::StructBuilder.cache[struct_definition.hash]
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'using plugins' do
|
91
|
+
include_context 'plugins'
|
92
|
+
|
93
|
+
before do
|
94
|
+
conn.alter_table :users do
|
95
|
+
add_column :created_at, :timestamp, null: false
|
96
|
+
add_column :updated_at, :timestamp, null: false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'allows to use plugins in generated commands' do
|
101
|
+
repo = Class.new(ROM::Repository[:users]) do
|
102
|
+
commands :create, update: :by_pk, use: :timestamps
|
103
|
+
end.new(rom)
|
104
|
+
|
105
|
+
user = repo.create(name: 'Jane')
|
106
|
+
expect(user.created_at).to be_within(1).of Time.now
|
107
|
+
expect(user.created_at).to eql(user.updated_at)
|
108
|
+
|
109
|
+
repo.update(user.id, **user, name: 'Jane Doe')
|
110
|
+
updated_user = repo.users.by_pk(user.id).one
|
111
|
+
expect(updated_user.created_at).to eql(user.created_at)
|
112
|
+
expect(updated_user.updated_at).to be > updated_user.created_at
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'allows to use several plugins' do
|
116
|
+
repo = Class.new(ROM::Repository[:users]) do
|
117
|
+
commands :create, use: %i(upcase_name timestamps)
|
118
|
+
end.new(rom)
|
119
|
+
|
120
|
+
user = repo.create(name: 'Jane')
|
121
|
+
expect(user.created_at).to be_within(1).of Time.now
|
122
|
+
expect(user.name).to eql('JANE')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe 'using custom mappers' do
|
127
|
+
before do
|
128
|
+
configuration.mappers do
|
129
|
+
register :users,
|
130
|
+
name_list: -> users { users.map { |u| u[:name] } },
|
131
|
+
id_list: -> users { users.map { |u| u[:id] } }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'allows to use named mapper in commands' do
|
136
|
+
repo = Class.new(ROM::Repository[:users]).new(rom)
|
137
|
+
|
138
|
+
name = repo.command(create: :users, mapper: :name_list).call(name: 'Jane')
|
139
|
+
|
140
|
+
expect(name).to eql('Jane')
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'caches command pipeline using mapper option' do
|
144
|
+
repo = Class.new(ROM::Repository[:users]).new(rom)
|
145
|
+
|
146
|
+
c1 = repo.command(create: :users, mapper: :name_list)
|
147
|
+
c2 = repo.command(create: :users, mapper: :name_list)
|
148
|
+
c3 = repo.command(create: :users, mapper: :id_list)
|
149
|
+
|
150
|
+
name = c1.call(name: 'Jane')
|
151
|
+
id = c3.call(name: 'John')
|
152
|
+
|
153
|
+
expect(c1).to be c2
|
154
|
+
expect(c3).not_to be c1
|
155
|
+
|
156
|
+
expect(name).to eql('Jane')
|
157
|
+
expect(id).to eql(2)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'allows to set a mapper with a class-level macro' do
|
161
|
+
repo = Class.new(ROM::Repository[:users]) do
|
162
|
+
commands :create, update: :by_pk, delete: :by_pk, mapper: :name_list
|
163
|
+
end.new(rom)
|
164
|
+
|
165
|
+
name = repo.create(name: 'Jane')
|
166
|
+
expect(name).to eql('Jane')
|
167
|
+
|
168
|
+
updated_name = repo.update(1, name: 'Jane Doe')
|
169
|
+
expect(updated_name).to eql('Jane Doe')
|
170
|
+
|
171
|
+
deleted_name = repo.delete(1)
|
172
|
+
expect(deleted_name).to eql('Jane Doe')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
RSpec.describe ROM::Repository, '#command' do
|
2
|
+
include_context 'database'
|
3
|
+
include_context 'relations'
|
4
|
+
include_context 'repo'
|
5
|
+
|
6
|
+
context 'accessing custom command from the registry' do
|
7
|
+
before do
|
8
|
+
configuration.commands(:users) do
|
9
|
+
define(:upsert, type: ROM::SQL::Commands::Create)
|
10
|
+
define(:create)
|
11
|
+
end
|
12
|
+
|
13
|
+
configuration.commands(:tasks) do
|
14
|
+
define(:create)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'returns registered command' do
|
19
|
+
expect(repo.command(:users).upsert).to be(rom.command(:users).upsert)
|
20
|
+
expect(repo.command(:users)[:upsert]).to be(rom.command(:users).upsert)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'exposes command builder DSL' do
|
24
|
+
command = repo.command.create(user: :users) { |user| user.create(:tasks) }
|
25
|
+
|
26
|
+
expect(command).to be_instance_of(ROM::Command::Graph)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context ':create' do
|
31
|
+
it 'builds Create command for a relation' do
|
32
|
+
create_user = repo.command(create: :users)
|
33
|
+
|
34
|
+
user = create_user.call(name: 'Jane Doe')
|
35
|
+
|
36
|
+
expect(user.id).to_not be(nil)
|
37
|
+
expect(user.name).to eql('Jane Doe')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'uses schema_hash for command input' do
|
41
|
+
create_user = repo.command(create: :users)
|
42
|
+
expect(create_user.input).to eql(repo.users.schema_hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'caches commands' do
|
46
|
+
create_user = -> { repo.command(create: :users) }
|
47
|
+
|
48
|
+
expect(create_user.()).to be(create_user.())
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'builds Create command for a relation graph with one-to-one' do
|
52
|
+
create_user = repo.command(
|
53
|
+
:create,
|
54
|
+
repo.users.combine_children(one: repo.tasks)
|
55
|
+
)
|
56
|
+
|
57
|
+
user = create_user.call(name: 'Jane Doe', task: { title: 'Task one' })
|
58
|
+
|
59
|
+
expect(user.id).to_not be(nil)
|
60
|
+
expect(user.name).to eql('Jane Doe')
|
61
|
+
expect(user.task.title).to eql('Task one')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'builds Create command for a deeply nested relation graph' do
|
65
|
+
create_user = repo.command(
|
66
|
+
:create,
|
67
|
+
repo.users.combine_children(one: repo.tasks.combine_children(many: repo.tags))
|
68
|
+
)
|
69
|
+
|
70
|
+
user = create_user.call(
|
71
|
+
name: 'Jane Doe', task: { title: 'Task one', tags: [{ name: 'red' }] }
|
72
|
+
)
|
73
|
+
|
74
|
+
expect(user.id).to_not be(nil)
|
75
|
+
expect(user.name).to eql('Jane Doe')
|
76
|
+
expect(user.task.title).to eql('Task one')
|
77
|
+
expect(user.task.tags).to be_instance_of(Array)
|
78
|
+
expect(user.task.tags.first.name).to eql('red')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'builds Create command for a relation graph with one-to-many' do
|
82
|
+
create_user = repo.command(
|
83
|
+
:create,
|
84
|
+
repo.users.combine_children(many: repo.tasks)
|
85
|
+
)
|
86
|
+
|
87
|
+
user = create_user.call(name: 'Jane Doe', tasks: [{ title: 'Task one' }])
|
88
|
+
|
89
|
+
expect(user.id).to_not be(nil)
|
90
|
+
expect(user.name).to eql('Jane Doe')
|
91
|
+
expect(user.tasks).to be_instance_of(Array)
|
92
|
+
expect(user.tasks.first.title).to eql('Task one')
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'builds Create command for a deeply nested graph with one-to-many' do
|
96
|
+
create_user = repo.command(
|
97
|
+
:create,
|
98
|
+
repo.aggregate(many: repo.tasks.combine_children(many: repo.tags))
|
99
|
+
)
|
100
|
+
|
101
|
+
user = create_user.call(
|
102
|
+
name: 'Jane',
|
103
|
+
tasks: [{ title: 'Task', tags: [{ name: 'red' }]}]
|
104
|
+
)
|
105
|
+
|
106
|
+
expect(user.id).to_not be(nil)
|
107
|
+
expect(user.name).to eql('Jane')
|
108
|
+
expect(user.tasks).to be_instance_of(Array)
|
109
|
+
expect(user.tasks.first.title).to eql('Task')
|
110
|
+
expect(user.tasks.first.tags).to be_instance_of(Array)
|
111
|
+
expect(user.tasks.first.tags.first.name).to eql('red')
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'builds Create command for a deeply nested graph with many-to-one & one-to-many' do
|
115
|
+
create_user = repo.command(
|
116
|
+
:create,
|
117
|
+
repo.aggregate(one: repo.tasks.combine_children(many: repo.tags))
|
118
|
+
)
|
119
|
+
|
120
|
+
user = create_user.call(
|
121
|
+
name: 'Jane', task: { title: 'Task', tags: [{ name: 'red' }, { name: 'blue' }] }
|
122
|
+
)
|
123
|
+
|
124
|
+
expect(user.id).to_not be(nil)
|
125
|
+
expect(user.name).to eql('Jane')
|
126
|
+
expect(user.task.title).to eql('Task')
|
127
|
+
expect(user.task.tags).to be_instance_of(Array)
|
128
|
+
expect(user.task.tags.size).to be(2)
|
129
|
+
expect(user.task.tags[0].name).to eql('red')
|
130
|
+
expect(user.task.tags[1].name).to eql('blue')
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'builds Create command for a deeply nested graph with many-to-one' do
|
134
|
+
create_user = repo.command(
|
135
|
+
:create,
|
136
|
+
repo.aggregate(one: repo.tasks.combine_children(one: repo.tags))
|
137
|
+
)
|
138
|
+
|
139
|
+
user = create_user.call(
|
140
|
+
name: 'Jane', task: { title: 'Task', tag: { name: 'red' } }
|
141
|
+
)
|
142
|
+
|
143
|
+
expect(user.id).to_not be(nil)
|
144
|
+
expect(user.name).to eql('Jane')
|
145
|
+
expect(user.task.id).to_not be(nil)
|
146
|
+
expect(user.task.title).to eql('Task')
|
147
|
+
expect(user.task.tag.id).to_not be(nil)
|
148
|
+
expect(user.task.tag.name).to eql('red')
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'builds Create command for a nested graph with many-to-many' do
|
152
|
+
user = repo.command(:create, repo.users).(name: 'Jane')
|
153
|
+
|
154
|
+
create_post = repo.command(
|
155
|
+
:create, repo.posts.combine_children(many: { labels: repo.labels })
|
156
|
+
)
|
157
|
+
|
158
|
+
post = create_post.call(
|
159
|
+
author_id: user.id, title: 'Jane post', labels: [{ name: 'red' }]
|
160
|
+
)
|
161
|
+
|
162
|
+
expect(post.labels.size).to be(1)
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'relation with a custom dataset name' do
|
166
|
+
it 'allows configuring a create command' do
|
167
|
+
create_comment = comments_repo.command(create: :comments)
|
168
|
+
|
169
|
+
comment = create_comment.(author: 'gerybabooma', body: 'DIS GUY MUST BE A ALIEN OR SUTIN')
|
170
|
+
|
171
|
+
expect(comment.message_id).to eql(1)
|
172
|
+
expect(comment.author).to eql('gerybabooma')
|
173
|
+
expect(comment.body).to eql('DIS GUY MUST BE A ALIEN OR SUTIN')
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'allows configuring a create command with aliased one-to-many' do
|
177
|
+
pending 'custom association names in the input are not yet support'
|
178
|
+
|
179
|
+
create_comment = comments_repo.command(:create, comments_repo.comments.combine(:emotions))
|
180
|
+
|
181
|
+
comment = create_comment.(author: 'Jane',
|
182
|
+
body: 'Hello Joe',
|
183
|
+
emotions: [{ author: 'Joe' }])
|
184
|
+
|
185
|
+
expect(comment.message_id).to eql(1)
|
186
|
+
expect(comment.author).to eql('Jane')
|
187
|
+
expect(comment.body).to eql('Hello Joe')
|
188
|
+
expect(comment.emotions.size).to eql(1)
|
189
|
+
expect(comment.emotions[0].author).to eql('Joe')
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context ':update' do
|
195
|
+
it 'builds Update command for a relation' do
|
196
|
+
repo.users.insert(id: 3, name: 'Jane')
|
197
|
+
|
198
|
+
update_user = repo.command(:update, repo.users)
|
199
|
+
|
200
|
+
user = update_user.by_pk(3).call(name: 'Jane Doe')
|
201
|
+
|
202
|
+
expect(user.id).to be(3)
|
203
|
+
expect(user.name).to eql('Jane Doe')
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context ':delete' do
|
208
|
+
it 'builds Delete command for a relation' do
|
209
|
+
repo.users.insert(id: 3, name: 'Jane')
|
210
|
+
|
211
|
+
delete_user = repo.command(:delete, repo.users)
|
212
|
+
|
213
|
+
delete_user.by_pk(3).call
|
214
|
+
|
215
|
+
expect(repo.users.by_pk(3).one).to be(nil)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'raises error when unsupported type is used' do
|
220
|
+
expect { repo.command(:oops, repo.users) }.to raise_error(
|
221
|
+
ArgumentError, /oops/
|
222
|
+
)
|
223
|
+
end
|
224
|
+
end
|
@@ -37,11 +37,11 @@ RSpec.describe 'Repository with multi-adapters configuration' do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
class Repository < ROM::Repository
|
41
|
-
relations :
|
40
|
+
class Repository < ROM::Repository[:sql_users]
|
41
|
+
relations :memory_tasks
|
42
42
|
|
43
43
|
def users_with_tasks(id)
|
44
|
-
|
44
|
+
aggregate(many: { tasks: memory_tasks }).where(id: id)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
@@ -8,12 +8,24 @@ RSpec.describe 'ROM repository' do
|
|
8
8
|
expect(repo.all_users.to_a).to match_array([jane, joe])
|
9
9
|
end
|
10
10
|
|
11
|
+
it 'can be used with a custom mapper' do
|
12
|
+
expect(repo.all_users_as_users.to_a).to match_array([
|
13
|
+
Test::Models::User.new(jane),
|
14
|
+
Test::Models::User.new(joe)
|
15
|
+
])
|
16
|
+
end
|
17
|
+
|
11
18
|
it 'loads a relation by an association' do
|
12
19
|
expect(repo.tasks_for_users(repo.all_users)).to match_array([jane_task, joe_task])
|
13
20
|
end
|
14
21
|
|
15
22
|
it 'loads a combine relation with one parent' do
|
16
|
-
|
23
|
+
task = repo.task_with_user.first
|
24
|
+
|
25
|
+
expect(task.id).to eql(task_with_user.id)
|
26
|
+
expect(task.title).to eql(task_with_user.title)
|
27
|
+
expect(task.user.id).to eql(task_with_user.user.id)
|
28
|
+
expect(task.user.name).to eql(task_with_user.user.name)
|
17
29
|
end
|
18
30
|
|
19
31
|
it 'loads a combine relation with one parent with custom tuple key' do
|
@@ -35,10 +47,93 @@ RSpec.describe 'ROM repository' do
|
|
35
47
|
end
|
36
48
|
|
37
49
|
it 'loads nested combined relations' do
|
38
|
-
expect(repo.users_with_tasks_and_tags.first).to eql(user_with_task_and_tags)
|
50
|
+
expect(repo.users_with_tasks_and_tags.first.to_h).to eql(user_with_task_and_tags.to_h)
|
39
51
|
end
|
40
52
|
|
41
53
|
it 'loads a wrapped relation' do
|
42
54
|
expect(repo.tag_with_wrapped_task.first).to eql(tag_with_task)
|
43
55
|
end
|
56
|
+
|
57
|
+
it 'loads an aggregate via custom fks' do
|
58
|
+
jane = repo.aggregate(many: repo.posts).where(name: 'Jane').one
|
59
|
+
|
60
|
+
expect(jane.posts.size).to be(1)
|
61
|
+
expect(jane.posts.first.title).to eql('Hello From Jane')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'loads an aggregate via assoc name' do
|
65
|
+
jane = repo.aggregate(:posts).where(name: 'Jane').one
|
66
|
+
|
67
|
+
expect(jane.posts.size).to be(1)
|
68
|
+
expect(jane.posts.first.title).to eql('Hello From Jane')
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'loads an aggregate with multiple associations' do
|
72
|
+
jane = repo.aggregate(:posts, :labels).where(name: 'Jane').one
|
73
|
+
|
74
|
+
expect(jane.posts.size).to be(1)
|
75
|
+
expect(jane.posts.first.title).to eql('Hello From Jane')
|
76
|
+
|
77
|
+
expect(jane.labels.size).to be(2)
|
78
|
+
expect(jane.labels[0].name).to eql('red')
|
79
|
+
expect(jane.labels[1].name).to eql('blue')
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'loads a parent via custom fks' do
|
83
|
+
post = repo.posts.combine(:author).where(title: 'Hello From Jane').one
|
84
|
+
|
85
|
+
expect(post.title).to eql('Hello From Jane')
|
86
|
+
expect(post.author.name).to eql('Jane')
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'loads aggregate through many-to-many via custom options' do
|
90
|
+
post = repo.posts
|
91
|
+
.combine_children(many: repo.labels)
|
92
|
+
.where(title: 'Hello From Jane')
|
93
|
+
.one
|
94
|
+
|
95
|
+
expect(post.title).to eql('Hello From Jane')
|
96
|
+
expect(post.labels.size).to be(2)
|
97
|
+
expect(post.labels.map(&:name)).to eql(%w(red blue))
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'loads aggregate through many-to-many association' do
|
101
|
+
post = repo.posts.combine(:labels).where(title: 'Hello From Jane').one
|
102
|
+
|
103
|
+
expect(post.title).to eql('Hello From Jane')
|
104
|
+
expect(post.labels.size).to be(2)
|
105
|
+
expect(post.labels.map(&:name)).to eql(%w(red blue))
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'not common naming conventions' do
|
109
|
+
it 'still loads nested relations' do
|
110
|
+
comments = comments_repo.comments_with_likes.to_a
|
111
|
+
|
112
|
+
expect(comments.size).to be(2)
|
113
|
+
expect(comments[0].author).to eql('Jane')
|
114
|
+
expect(comments[0].likes[0].author).to eql('Joe')
|
115
|
+
expect(comments[0].likes[1].author).to eql('Anonymous')
|
116
|
+
expect(comments[1].author).to eql('Joe')
|
117
|
+
expect(comments[1].likes[0].author).to eql('Jane')
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'loads nested relations by association name' do
|
121
|
+
comments = comments_repo.comments_with_emotions.to_a
|
122
|
+
|
123
|
+
expect(comments.size).to be(2)
|
124
|
+
expect(comments[0].emotions[0].author).to eql('Joe')
|
125
|
+
expect(comments[0].emotions[1].author).to eql('Anonymous')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'with a table without columns' do
|
130
|
+
before { conn.create_table(:dummy) unless conn.table_exists?(:dummy) }
|
131
|
+
|
132
|
+
it 'does not fail with a weird error when a relation does not have attributes' do
|
133
|
+
configuration.relation(:dummy)
|
134
|
+
|
135
|
+
repo = Class.new(ROM::Repository[:dummy]).new(rom)
|
136
|
+
expect(repo.dummy.to_a).to eql([])
|
137
|
+
end
|
138
|
+
end
|
44
139
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
RSpec.describe ROM::Repository::Root do
|
2
|
+
subject(:repo) do
|
3
|
+
Class.new(ROM::Repository[:users]) do
|
4
|
+
relations :tasks, :posts, :labels
|
5
|
+
end.new(rom)
|
6
|
+
end
|
7
|
+
|
8
|
+
include_context 'database'
|
9
|
+
include_context 'relations'
|
10
|
+
|
11
|
+
describe '.[]' do
|
12
|
+
it 'creates a pre-configured root repo class' do
|
13
|
+
klass = ROM::Repository[:users]
|
14
|
+
|
15
|
+
expect(klass.relations).to eql([:users])
|
16
|
+
expect(klass.root).to be(:users)
|
17
|
+
|
18
|
+
child = klass[:users]
|
19
|
+
|
20
|
+
expect(child.relations).to eql([:users])
|
21
|
+
expect(child.root).to be(:users)
|
22
|
+
expect(child < klass).to be(true)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'inheritance' do
|
27
|
+
it 'inherits root and relations' do
|
28
|
+
klass = Class.new(repo.class)
|
29
|
+
|
30
|
+
expect(klass.relations).to eql([:users, :tasks, :posts, :labels])
|
31
|
+
expect(klass.root).to be(:users)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'creates base root class' do
|
35
|
+
klass = Class.new(ROM::Repository)[:users]
|
36
|
+
|
37
|
+
expect(klass.relations).to eql([:users])
|
38
|
+
expect(klass.root).to be(:users)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#root' do
|
43
|
+
it 'returns configured root relation' do
|
44
|
+
expect(repo.root.relation).to be(rom.relations[:users])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#aggregate' do
|
49
|
+
include_context 'seeds'
|
50
|
+
|
51
|
+
it 'builds an aggregate from the root relation and other relation(s)' do
|
52
|
+
user = repo.aggregate(many: repo.tasks).where(name: 'Jane').one
|
53
|
+
|
54
|
+
expect(user.name).to eql('Jane')
|
55
|
+
expect(user.tasks.size).to be(1)
|
56
|
+
expect(user.tasks[0].title).to eql('Jane Task')
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with associations' do
|
60
|
+
it 'builds an aggregate from a canonical association' do
|
61
|
+
user = repo.aggregate(:labels).where(name: 'Joe').one
|
62
|
+
|
63
|
+
expect(user.name).to eql('Joe')
|
64
|
+
expect(user.labels.size).to be(1)
|
65
|
+
expect(user.labels[0].author_id).to be(user.id)
|
66
|
+
expect(user.labels[0].name).to eql('green')
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'builds a command from an aggregate' do
|
70
|
+
command = repo.command(:create, repo.aggregate(:posts))
|
71
|
+
|
72
|
+
result = command.call(name: 'Jade', posts: [{ title: 'Jade post' }])
|
73
|
+
|
74
|
+
expect(result.name).to eql('Jade')
|
75
|
+
expect(result.posts.size).to be(1)
|
76
|
+
expect(result.posts[0].author_id).to be(result.id)
|
77
|
+
expect(result.posts[0].title).to eql('Jade post')
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'builds same relation as manual combine' do
|
81
|
+
left = repo.aggregate(:posts)
|
82
|
+
right = repo.users.combine_children(many: repo.posts)
|
83
|
+
|
84
|
+
expect(left.to_ast).to eql(right.to_ast)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/spec/shared/database.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
RSpec.shared_context 'database' do
|
2
|
-
let(:configuration) { ROM::Configuration.new(:sql, uri)
|
2
|
+
let(:configuration) { ROM::Configuration.new(:sql, uri) }
|
3
3
|
let(:conn) { configuration.gateways[:default].connection }
|
4
4
|
let(:rom) { ROM.container(configuration) }
|
5
|
-
let(:uri)
|
5
|
+
let(:uri) do
|
6
|
+
if defined? JRUBY_VERSION
|
7
|
+
'jdbc:postgresql://localhost/rom_repository'
|
8
|
+
else
|
9
|
+
'postgres://localhost/rom_repository'
|
10
|
+
end
|
11
|
+
end
|
6
12
|
|
7
13
|
before do
|
8
14
|
conn.loggers << LOGGER
|
9
15
|
|
10
|
-
[:tags, :tasks, :users
|
16
|
+
[:tags, :tasks, :posts, :users, :posts_labels, :labels, :books,
|
17
|
+
:reactions, :messages].each { |table| conn.drop_table?(table) }
|
18
|
+
|
19
|
+
conn.create_table :books do
|
20
|
+
primary_key :id
|
21
|
+
column :title, String
|
22
|
+
column :created_at, Time
|
23
|
+
column :updated_at, Time
|
24
|
+
end
|
11
25
|
|
12
26
|
conn.create_table :users do
|
13
27
|
primary_key :id
|
@@ -25,5 +39,35 @@ RSpec.shared_context 'database' do
|
|
25
39
|
foreign_key :task_id, :tasks, null: false, on_delete: :cascade
|
26
40
|
column :name, String
|
27
41
|
end
|
42
|
+
|
43
|
+
conn.create_table :labels do
|
44
|
+
primary_key :id
|
45
|
+
column :name, String
|
46
|
+
end
|
47
|
+
|
48
|
+
conn.create_table :posts do
|
49
|
+
primary_key :id
|
50
|
+
foreign_key :author_id, :users, null: false, on_delete: :cascade
|
51
|
+
column :title, String
|
52
|
+
column :body, String
|
53
|
+
end
|
54
|
+
|
55
|
+
conn.create_table :posts_labels do
|
56
|
+
foreign_key :post_id, :labels, null: false, on_delete: :cascade
|
57
|
+
foreign_key :label_id, :labels, null: false, on_delete: :cascade
|
58
|
+
primary_key [:post_id, :label_id]
|
59
|
+
end
|
60
|
+
|
61
|
+
conn.create_table :messages do
|
62
|
+
primary_key :message_id
|
63
|
+
column :author, String
|
64
|
+
column :body, String
|
65
|
+
end
|
66
|
+
|
67
|
+
conn.create_table :reactions do
|
68
|
+
primary_key :reaction_id
|
69
|
+
foreign_key :message_id, :messages, null: false
|
70
|
+
column :author, String
|
71
|
+
end
|
28
72
|
end
|
29
73
|
end
|