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.
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