rom-repository 0.2.0 → 0.3.0
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 +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
|