rom-repository 0.3.1 → 1.0.0.beta1
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/.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
|