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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -5
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +21 -5
  6. data/README.md +6 -110
  7. data/lib/rom/repository/changeset/create.rb +26 -0
  8. data/lib/rom/repository/changeset/pipe.rb +40 -0
  9. data/lib/rom/repository/changeset/update.rb +82 -0
  10. data/lib/rom/repository/changeset.rb +99 -0
  11. data/lib/rom/repository/class_interface.rb +142 -0
  12. data/lib/rom/repository/command_compiler.rb +214 -0
  13. data/lib/rom/repository/command_proxy.rb +22 -0
  14. data/lib/rom/repository/header_builder.rb +13 -16
  15. data/lib/rom/repository/mapper_builder.rb +7 -14
  16. data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
  17. data/lib/rom/repository/relation_proxy.rb +225 -0
  18. data/lib/rom/repository/root.rb +110 -0
  19. data/lib/rom/repository/struct_attributes.rb +46 -0
  20. data/lib/rom/repository/struct_builder.rb +31 -14
  21. data/lib/rom/repository/version.rb +1 -1
  22. data/lib/rom/repository.rb +192 -31
  23. data/lib/rom/struct.rb +13 -8
  24. data/rom-repository.gemspec +9 -10
  25. data/spec/integration/changeset_spec.rb +86 -0
  26. data/spec/integration/command_macros_spec.rb +175 -0
  27. data/spec/integration/command_spec.rb +224 -0
  28. data/spec/integration/multi_adapter_spec.rb +3 -3
  29. data/spec/integration/repository_spec.rb +97 -2
  30. data/spec/integration/root_repository_spec.rb +88 -0
  31. data/spec/shared/database.rb +47 -3
  32. data/spec/shared/mappers.rb +35 -0
  33. data/spec/shared/models.rb +41 -0
  34. data/spec/shared/plugins.rb +66 -0
  35. data/spec/shared/relations.rb +76 -0
  36. data/spec/shared/repo.rb +38 -17
  37. data/spec/shared/seeds.rb +19 -0
  38. data/spec/spec_helper.rb +4 -1
  39. data/spec/support/mapper_registry.rb +1 -3
  40. data/spec/unit/changeset_spec.rb +58 -0
  41. data/spec/unit/header_builder_spec.rb +34 -35
  42. data/spec/unit/relation_proxy_spec.rb +170 -0
  43. data/spec/unit/sql/relation_spec.rb +5 -5
  44. data/spec/unit/struct_builder_spec.rb +7 -4
  45. data/spec/unit/struct_spec.rb +22 -0
  46. metadata +38 -41
  47. data/lib/rom/plugins/relation/key_inference.rb +0 -31
  48. data/lib/rom/repository/loading_proxy/combine.rb +0 -158
  49. data/lib/rom/repository/loading_proxy.rb +0 -182
  50. 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 :sql_users, :memory_tasks
40
+ class Repository < ROM::Repository[:sql_users]
41
+ relations :memory_tasks
42
42
 
43
43
  def users_with_tasks(id)
44
- sql_users.where(id: id).combine_children(many: { tasks: memory_tasks })
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
- expect(repo.task_with_user.first).to eql(task_with_user)
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
@@ -1,13 +1,27 @@
1
1
  RSpec.shared_context 'database' do
2
- let(:configuration) { ROM::Configuration.new(:sql, uri).use(:macros) }
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) { 'postgres://localhost/rom_repository' }
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].each { |table| conn.drop_table?(table) }
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