rom-repository 0.3.1 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +11 -13
- data/CHANGELOG.md +25 -0
- data/Gemfile +13 -3
- data/lib/rom/repository.rb +57 -19
- data/lib/rom/repository/changeset.rb +89 -26
- data/lib/rom/repository/changeset/create.rb +34 -0
- data/lib/rom/repository/changeset/delete.rb +15 -0
- data/lib/rom/repository/changeset/pipe.rb +11 -4
- data/lib/rom/repository/changeset/update.rb +11 -1
- data/lib/rom/repository/command_compiler.rb +51 -30
- data/lib/rom/repository/command_proxy.rb +3 -1
- data/lib/rom/repository/header_builder.rb +3 -3
- data/lib/rom/repository/mapper_builder.rb +2 -2
- data/lib/rom/repository/relation_proxy.rb +26 -35
- data/lib/rom/repository/relation_proxy/combine.rb +59 -27
- data/lib/rom/repository/root.rb +4 -6
- data/lib/rom/repository/session.rb +55 -0
- data/lib/rom/repository/struct_builder.rb +29 -17
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/struct.rb +11 -20
- data/rom-repository.gemspec +4 -3
- data/spec/integration/command_macros_spec.rb +5 -2
- data/spec/integration/command_spec.rb +0 -6
- data/spec/integration/multi_adapter_spec.rb +8 -5
- data/spec/integration/repository_spec.rb +58 -2
- data/spec/integration/root_repository_spec.rb +9 -2
- data/spec/integration/typed_structs_spec.rb +31 -0
- data/spec/shared/database.rb +5 -1
- data/spec/shared/relations.rb +3 -1
- data/spec/shared/repo.rb +13 -1
- data/spec/shared/structs.rb +39 -0
- data/spec/spec_helper.rb +7 -5
- data/spec/support/mutant.rb +10 -0
- data/spec/unit/changeset/map_spec.rb +42 -0
- data/spec/unit/changeset_spec.rb +32 -6
- data/spec/unit/relation_proxy_spec.rb +27 -9
- data/spec/unit/repository/changeset_spec.rb +125 -0
- data/spec/unit/repository/inspect_spec.rb +18 -0
- data/spec/unit/repository/session_spec.rb +251 -0
- data/spec/unit/session_spec.rb +54 -0
- data/spec/unit/struct_builder_spec.rb +45 -1
- metadata +41 -17
- data/lib/rom/repository/struct_attributes.rb +0 -46
- data/spec/unit/header_builder_spec.rb +0 -73
- data/spec/unit/plugins/view_spec.rb +0 -29
- data/spec/unit/sql/relation_spec.rb +0 -54
- data/spec/unit/struct_spec.rb +0 -22
data/lib/rom/repository/root.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'dry/core/class_attributes'
|
2
|
+
|
1
3
|
module ROM
|
2
4
|
class Repository
|
3
5
|
# A specialized repository type dedicated to work with a root relation
|
@@ -27,7 +29,7 @@ module ROM
|
|
27
29
|
#
|
28
30
|
# @api public
|
29
31
|
class Root < Repository
|
30
|
-
extend
|
32
|
+
extend Dry::Core::ClassAttributes
|
31
33
|
|
32
34
|
defines :root
|
33
35
|
|
@@ -70,11 +72,7 @@ module ROM
|
|
70
72
|
#
|
71
73
|
# @api public
|
72
74
|
def aggregate(*args)
|
73
|
-
|
74
|
-
root.combine_children(args[0])
|
75
|
-
else
|
76
|
-
root.combine(*args)
|
77
|
-
end
|
75
|
+
root.combine(*args)
|
78
76
|
end
|
79
77
|
|
80
78
|
# @overload changeset(name, *args)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'dry/equalizer'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Session
|
5
|
+
include Dry::Equalizer(:queue, :status)
|
6
|
+
|
7
|
+
attr_reader :repo
|
8
|
+
|
9
|
+
attr_reader :queue
|
10
|
+
|
11
|
+
attr_reader :status
|
12
|
+
|
13
|
+
def initialize(repo)
|
14
|
+
@repo = repo
|
15
|
+
@status = :pending
|
16
|
+
initialize_queue!
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(changeset)
|
20
|
+
queue << changeset
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def commit!
|
25
|
+
queue.map(&:command).compact.each(&:call)
|
26
|
+
|
27
|
+
@status = :success
|
28
|
+
|
29
|
+
self
|
30
|
+
rescue => e
|
31
|
+
@status = :failure
|
32
|
+
raise e
|
33
|
+
ensure
|
34
|
+
initialize_queue!
|
35
|
+
end
|
36
|
+
|
37
|
+
def pending?
|
38
|
+
status.equal?(:pending)
|
39
|
+
end
|
40
|
+
|
41
|
+
def success?
|
42
|
+
status.equal?(:success)
|
43
|
+
end
|
44
|
+
|
45
|
+
def failure?
|
46
|
+
status.equal?(:failure)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def initialize_queue!
|
52
|
+
@queue = []
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,24 +1,26 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
require '
|
4
|
-
require 'rom/support/constants'
|
5
|
-
require 'rom/support/class_builder'
|
1
|
+
require 'dry/core/inflector'
|
2
|
+
require 'dry/core/cache'
|
3
|
+
require 'dry/core/class_builder'
|
6
4
|
|
7
|
-
require 'rom/
|
5
|
+
require 'rom/struct'
|
6
|
+
require 'rom/schema/type'
|
8
7
|
|
9
8
|
module ROM
|
10
9
|
class Repository
|
11
10
|
# @api private
|
12
11
|
class StructBuilder
|
13
|
-
extend Cache
|
12
|
+
extend Dry::Core::Cache
|
14
13
|
|
15
14
|
def call(*args)
|
16
15
|
fetch_or_store(*args) do
|
17
16
|
name, header = args
|
17
|
+
attributes = visit(header).compact
|
18
18
|
|
19
|
-
build_class(name)
|
20
|
-
|
21
|
-
|
19
|
+
build_class(name, ROM::Struct) do |klass|
|
20
|
+
attributes.each do |(name, type)|
|
21
|
+
klass.attribute(name, type)
|
22
|
+
end
|
23
|
+
end
|
22
24
|
end
|
23
25
|
end
|
24
26
|
alias_method :[], :call
|
@@ -35,20 +37,30 @@ module ROM
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def visit_relation(node)
|
38
|
-
relation_name, meta,
|
39
|
-
meta[:combine_name] || relation_name.relation
|
40
|
+
relation_name, meta, header = node
|
41
|
+
name = meta[:combine_name] || relation_name.relation
|
42
|
+
|
43
|
+
model = call(name, header)
|
44
|
+
|
45
|
+
if meta[:combine_type] == :many
|
46
|
+
[name, Types::Array.member(model)]
|
47
|
+
else
|
48
|
+
[name, model.optional]
|
49
|
+
end
|
40
50
|
end
|
41
51
|
|
42
|
-
def visit_attribute(
|
43
|
-
|
52
|
+
def visit_attribute(attr)
|
53
|
+
unless attr.foreign_key?
|
54
|
+
[attr.aliased? && !attr.wrapped? ? attr.alias : attr.name, attr.type]
|
55
|
+
end
|
44
56
|
end
|
45
57
|
|
46
|
-
def build_class(name, &block)
|
47
|
-
|
58
|
+
def build_class(name, parent, &block)
|
59
|
+
Dry::Core::ClassBuilder.new(name: class_name(name), parent: parent).call(&block)
|
48
60
|
end
|
49
61
|
|
50
62
|
def class_name(name)
|
51
|
-
"ROM::Struct[#{Inflector.classify(Inflector.singularize(name))}]"
|
63
|
+
"ROM::Struct[#{Dry::Core::Inflector.classify(Dry::Core::Inflector.singularize(name))}]"
|
52
64
|
end
|
53
65
|
end
|
54
66
|
end
|
data/lib/rom/struct.rb
CHANGED
@@ -1,37 +1,28 @@
|
|
1
|
+
require 'dry/struct'
|
2
|
+
|
1
3
|
module ROM
|
2
4
|
# Simple data-struct
|
3
5
|
#
|
4
6
|
# By default mappers use this as the model
|
5
7
|
#
|
6
8
|
# @api public
|
7
|
-
class Struct
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# @return [Hash]
|
11
|
-
#
|
12
|
-
# @api private
|
13
|
-
def to_hash
|
14
|
-
to_h
|
15
|
-
end
|
16
|
-
|
17
|
-
# Reads an attribute value
|
18
|
-
#
|
19
|
-
# @param name [Symbol] The name of the attribute
|
9
|
+
class Struct < Dry::Struct
|
10
|
+
# Returns a short string representation
|
20
11
|
#
|
21
|
-
# @return [
|
12
|
+
# @return [String]
|
22
13
|
#
|
23
14
|
# @api public
|
24
|
-
def
|
25
|
-
|
15
|
+
def to_s
|
16
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
|
26
17
|
end
|
27
18
|
|
28
|
-
#
|
19
|
+
# Return attribute value
|
29
20
|
#
|
30
|
-
# @
|
21
|
+
# @param [Symbol] name The attribute name
|
31
22
|
#
|
32
23
|
# @api public
|
33
|
-
def
|
34
|
-
|
24
|
+
def fetch(name)
|
25
|
+
__send__(name)
|
35
26
|
end
|
36
27
|
end
|
37
28
|
end
|
data/rom-repository.gemspec
CHANGED
@@ -15,9 +15,10 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
16
16
|
gem.license = 'MIT'
|
17
17
|
|
18
|
-
gem.add_runtime_dependency 'rom', '~>
|
19
|
-
gem.add_runtime_dependency 'rom-
|
20
|
-
gem.add_runtime_dependency '
|
18
|
+
gem.add_runtime_dependency 'rom', '~> 3.0.0.beta'
|
19
|
+
gem.add_runtime_dependency 'rom-mapper', '~> 0.5.0.beta'
|
20
|
+
gem.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.1'
|
21
|
+
gem.add_runtime_dependency 'dry-struct', '~> 0.1'
|
21
22
|
|
22
23
|
gem.add_development_dependency 'rake', '~> 11.2'
|
23
24
|
gem.add_development_dependency 'rspec', '~> 3.5'
|
@@ -81,9 +81,12 @@ RSpec.describe ROM::Repository, '.command' do
|
|
81
81
|
|
82
82
|
user = repo.create(name: 'Jane')
|
83
83
|
|
84
|
-
expect(user).to be_kind_of
|
84
|
+
expect(user).to be_kind_of Dry::Struct
|
85
|
+
|
86
|
+
struct_definition = [:users, [:header, [
|
87
|
+
[:attribute, repo.users.schema[:id]],
|
88
|
+
[:attribute, repo.users.schema[:name]]]]]
|
85
89
|
|
86
|
-
struct_definition = [:users, [:header, [[:attribute, :id], [:attribute, :name]]]]
|
87
90
|
expect(user).to be_an_instance_of ROM::Repository::StructBuilder.cache[struct_definition.hash]
|
88
91
|
end
|
89
92
|
|
@@ -215,10 +215,4 @@ RSpec.describe ROM::Repository, '#command' do
|
|
215
215
|
expect(repo.users.by_pk(3).one).to be(nil)
|
216
216
|
end
|
217
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
218
|
end
|
@@ -15,21 +15,24 @@ RSpec.describe 'Repository with multi-adapters configuration' do
|
|
15
15
|
before do
|
16
16
|
module Test
|
17
17
|
class Users < ROM::Relation[:sql]
|
18
|
-
dataset :users
|
19
|
-
register_as :sql_users
|
20
18
|
gateway :default
|
19
|
+
schema(:users, infer: true)
|
20
|
+
register_as :sql_users
|
21
21
|
end
|
22
22
|
|
23
23
|
class Tasks < ROM::Relation[:memory]
|
24
|
-
|
24
|
+
schema(:tasks) do
|
25
|
+
attribute :user_id, ROM::Types::Int
|
26
|
+
attribute :title, ROM::Types::String
|
27
|
+
end
|
28
|
+
|
25
29
|
register_as :memory_tasks
|
26
30
|
gateway :memory
|
27
31
|
|
28
|
-
use :view
|
29
32
|
use :key_inference
|
30
33
|
|
31
34
|
view(:base, [:user_id, :title]) do
|
32
|
-
|
35
|
+
self
|
33
36
|
end
|
34
37
|
|
35
38
|
def for_users(users)
|
@@ -47,7 +47,23 @@ RSpec.describe 'ROM repository' do
|
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'loads nested combined relations' do
|
50
|
-
|
50
|
+
user = repo.users_with_tasks_and_tags.first
|
51
|
+
|
52
|
+
expect(user.id).to be(1)
|
53
|
+
expect(user.name).to eql('Jane')
|
54
|
+
expect(user.all_tasks.size).to be(1)
|
55
|
+
expect(user.all_tasks[0].id).to be(2)
|
56
|
+
expect(user.all_tasks[0].title).to eql('Jane Task')
|
57
|
+
expect(user.all_tasks[0].tags.size).to be(1)
|
58
|
+
expect(user.all_tasks[0].tags[0].name).to eql('red')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'loads nested combined relations using configured associations' do
|
62
|
+
jane = repo.users_with_posts_and_their_labels.first
|
63
|
+
|
64
|
+
expect(jane.posts.size).to be(1)
|
65
|
+
expect(jane.posts.map(&:title)).to eql(['Hello From Jane'])
|
66
|
+
expect(jane.posts.flat_map(&:labels).flat_map(&:name)).to eql(%w(red blue))
|
51
67
|
end
|
52
68
|
|
53
69
|
it 'loads a wrapped relation' do
|
@@ -105,6 +121,22 @@ RSpec.describe 'ROM repository' do
|
|
105
121
|
expect(post.labels.map(&:name)).to eql(%w(red blue))
|
106
122
|
end
|
107
123
|
|
124
|
+
it 'loads multiple child relations' do
|
125
|
+
user = repo.users.combine_children(many: [repo.posts, repo.tasks]).where(name: 'Jane').one
|
126
|
+
|
127
|
+
expect(user.name).to eql('Jane')
|
128
|
+
expect(user.posts.size).to be(1)
|
129
|
+
expect(user.posts[0].title).to eql('Hello From Jane')
|
130
|
+
expect(user.tasks.size).to be(1)
|
131
|
+
expect(user.tasks[0].title).to eql('Jane Task')
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'loads multiple parent relations' do
|
135
|
+
post_label = repo.posts_labels.combine_parents(one: [repo.posts]).first
|
136
|
+
|
137
|
+
expect(post_label.post.title).to eql('Hello From Jane')
|
138
|
+
end
|
139
|
+
|
108
140
|
context 'not common naming conventions' do
|
109
141
|
it 'still loads nested relations' do
|
110
142
|
comments = comments_repo.comments_with_likes.to_a
|
@@ -126,11 +158,35 @@ RSpec.describe 'ROM repository' do
|
|
126
158
|
end
|
127
159
|
end
|
128
160
|
|
161
|
+
describe 'projecting virtual attributes' do
|
162
|
+
it 'loads auto-mapped structs' do
|
163
|
+
user = repo.users.
|
164
|
+
inner_join(:posts, author_id: :id).
|
165
|
+
select_group { [id.qualified, name.qualified] }.
|
166
|
+
select_append { int::count(:posts).as(:post_count) }.
|
167
|
+
having { count(id.qualified) >= 1 }.
|
168
|
+
first
|
169
|
+
|
170
|
+
expect(user.id).to be(1)
|
171
|
+
expect(user.name).to eql('Jane')
|
172
|
+
expect(user.post_count).to be(1)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe 'projecting aliased attributes' do
|
177
|
+
it 'loads auto-mapped structs' do
|
178
|
+
user = repo.users.select { [id.aliased(:userId), name.aliased(:userName)] }.first
|
179
|
+
|
180
|
+
expect(user.userId).to be(1)
|
181
|
+
expect(user.userName).to eql('Jane')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
129
185
|
context 'with a table without columns' do
|
130
186
|
before { conn.create_table(:dummy) unless conn.table_exists?(:dummy) }
|
131
187
|
|
132
188
|
it 'does not fail with a weird error when a relation does not have attributes' do
|
133
|
-
configuration.relation(:dummy)
|
189
|
+
configuration.relation(:dummy) { schema(infer: true) }
|
134
190
|
|
135
191
|
repo = Class.new(ROM::Repository[:dummy]).new(rom)
|
136
192
|
expect(repo.dummy.to_a).to eql([])
|
@@ -62,10 +62,18 @@ RSpec.describe ROM::Repository::Root do
|
|
62
62
|
|
63
63
|
expect(user.name).to eql('Joe')
|
64
64
|
expect(user.labels.size).to be(1)
|
65
|
-
expect(user.labels[0].author_id).to be(user.id)
|
66
65
|
expect(user.labels[0].name).to eql('green')
|
67
66
|
end
|
68
67
|
|
68
|
+
it 'builds an aggregate with nesting level = 2' do
|
69
|
+
user = repo.aggregate(posts: [:labels, :author]).where(name: 'Joe').one
|
70
|
+
|
71
|
+
expect(user.name).to eql('Joe')
|
72
|
+
expect(user.posts.size).to be(1)
|
73
|
+
expect(user.posts[0].title).to eql('Hello From Joe')
|
74
|
+
expect(user.posts[0].labels.size).to be(1)
|
75
|
+
end
|
76
|
+
|
69
77
|
it 'builds a command from an aggregate' do
|
70
78
|
command = repo.command(:create, repo.aggregate(:posts))
|
71
79
|
|
@@ -73,7 +81,6 @@ RSpec.describe ROM::Repository::Root do
|
|
73
81
|
|
74
82
|
expect(result.name).to eql('Jade')
|
75
83
|
expect(result.posts.size).to be(1)
|
76
|
-
expect(result.posts[0].author_id).to be(result.id)
|
77
84
|
expect(result.posts[0].title).to eql('Jade post')
|
78
85
|
end
|
79
86
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
RSpec.describe 'ROM repository with typed structs' do
|
2
|
+
subject(:repo) do
|
3
|
+
Class.new(ROM::Repository[:books]).new(rom)
|
4
|
+
end
|
5
|
+
|
6
|
+
include_context 'database'
|
7
|
+
include_context 'seeds'
|
8
|
+
|
9
|
+
before do
|
10
|
+
configuration.relation(:books) do
|
11
|
+
schema(infer: true)
|
12
|
+
|
13
|
+
view(:index) do
|
14
|
+
schema { project(:id, :title, :created_at) }
|
15
|
+
relation { order(:title) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
rom.relations[:books].insert(title: 'Hello World', created_at: Time.now)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'loads typed structs' do
|
23
|
+
book = repo.books.index.first
|
24
|
+
|
25
|
+
expect(book).to be_kind_of(Dry::Struct)
|
26
|
+
|
27
|
+
expect(book.id).to be_kind_of(Integer)
|
28
|
+
expect(book.title).to eql('Hello World')
|
29
|
+
expect(book.created_at).to be_kind_of(Time)
|
30
|
+
end
|
31
|
+
end
|
data/spec/shared/database.rb
CHANGED
@@ -48,7 +48,7 @@ RSpec.shared_context 'database' do
|
|
48
48
|
conn.create_table :posts do
|
49
49
|
primary_key :id
|
50
50
|
foreign_key :author_id, :users, null: false, on_delete: :cascade
|
51
|
-
column :title, String
|
51
|
+
column :title, String, null: false
|
52
52
|
column :body, String
|
53
53
|
end
|
54
54
|
|
@@ -70,4 +70,8 @@ RSpec.shared_context 'database' do
|
|
70
70
|
column :author, String
|
71
71
|
end
|
72
72
|
end
|
73
|
+
|
74
|
+
after do
|
75
|
+
rom.disconnect
|
76
|
+
end
|
73
77
|
end
|