rom-repository 1.0.0.beta3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +3 -7
- data/CHANGELOG.md +1 -0
- data/README.md +1 -1
- data/lib/rom/repository/changeset/associated.rb +76 -0
- data/lib/rom/repository/changeset/create.rb +2 -52
- data/lib/rom/repository/changeset/delete.rb +7 -9
- data/lib/rom/repository/changeset/pipe.rb +3 -0
- data/lib/rom/repository/changeset/stateful.rb +240 -0
- data/lib/rom/repository/changeset/update.rb +11 -34
- data/lib/rom/repository/changeset.rb +64 -144
- data/lib/rom/repository/class_interface.rb +4 -4
- data/lib/rom/repository/command_compiler.rb +17 -15
- data/lib/rom/repository/command_proxy.rb +2 -0
- data/lib/rom/repository/relation_proxy/combine.rb +3 -4
- data/lib/rom/repository/relation_proxy.rb +24 -7
- data/lib/rom/repository/root.rb +14 -1
- data/lib/rom/repository/session.rb +3 -0
- data/lib/rom/repository/struct_builder.rb +1 -1
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/repository.rb +92 -32
- data/lib/rom/struct.rb +40 -2
- data/rom-repository.gemspec +2 -2
- data/spec/integration/changeset_spec.rb +27 -0
- data/spec/unit/changeset_spec.rb +45 -7
- data/spec/unit/repository/changeset_spec.rb +59 -22
- data/spec/unit/repository/transaction_spec.rb +15 -1
- metadata +8 -6
@@ -2,37 +2,65 @@ require 'dry/core/class_attributes'
|
|
2
2
|
require 'dry/core/cache'
|
3
3
|
|
4
4
|
require 'rom/initializer'
|
5
|
-
require 'rom/repository/changeset/pipe'
|
6
5
|
|
7
6
|
module ROM
|
7
|
+
# Abstract Changeset class
|
8
|
+
#
|
9
|
+
# If you inherit from this class you need to configure additional settings
|
10
|
+
#
|
11
|
+
# @example define a custom changeset using :upsert command
|
12
|
+
#
|
13
|
+
# class NewTag < ROM::Changeset[:tags]
|
14
|
+
# def default_command_type
|
15
|
+
# :upsert
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @abstract
|
8
20
|
class Changeset
|
21
|
+
DEFAULT_COMMAND_OPTS = { mapper: false }.freeze
|
22
|
+
|
9
23
|
extend Initializer
|
10
24
|
extend Dry::Core::Cache
|
11
25
|
extend Dry::Core::ClassAttributes
|
12
26
|
|
27
|
+
# @!method self.command_type
|
28
|
+
# Get or set changeset command type
|
29
|
+
#
|
30
|
+
# @overload command_type
|
31
|
+
# Return configured command_type
|
32
|
+
# @return [Symbol]
|
33
|
+
#
|
34
|
+
# @overload command_type(identifier)
|
35
|
+
# Set relation identifier for this changeset
|
36
|
+
# @param [Symbol] identifier The command type identifier
|
37
|
+
# @return [Symbol]
|
38
|
+
defines :command_type
|
39
|
+
|
40
|
+
# @!method self.relation
|
41
|
+
# Get or set changeset relation identifier
|
42
|
+
#
|
43
|
+
# @overload relation
|
44
|
+
# Return configured relation identifier for this changeset
|
45
|
+
# @return [Symbol]
|
46
|
+
#
|
47
|
+
# @overload relation(identifier)
|
48
|
+
# Set relation identifier for this changeset
|
49
|
+
# @param [Symbol] identifier The relation identifier from the ROM container
|
50
|
+
# @return [Symbol]
|
13
51
|
defines :relation
|
14
52
|
|
15
53
|
# @!attribute [r] relation
|
16
54
|
# @return [Relation] The changeset relation
|
17
55
|
param :relation
|
18
56
|
|
19
|
-
# @!attribute [r] __data__
|
20
|
-
# @return [Hash] The relation data
|
21
|
-
option :__data__, reader: true, optional: true, default: proc { nil }
|
22
|
-
|
23
|
-
# @!attribute [r] pipe
|
24
|
-
# @return [Changeset::Pipe] data transformation pipe
|
25
|
-
option :pipe, reader: true, accept: [Proc, Pipe], default: -> changeset {
|
26
|
-
changeset.class.default_pipe(changeset)
|
27
|
-
}
|
28
|
-
|
29
57
|
# @!attribute [r] command_compiler
|
30
58
|
# @return [Proc] a proc that can compile a command (typically provided by a repo)
|
31
59
|
option :command_compiler, reader: true, optional: true
|
32
60
|
|
33
61
|
# @!attribute [r] command_type
|
34
62
|
# @return [Symbol] a custom command identifier
|
35
|
-
option :command_type, reader: true,
|
63
|
+
option :command_type, reader: true, default: -> changeset { changeset.class.command_type }
|
36
64
|
|
37
65
|
# Create a changeset class preconfigured for a specific relation
|
38
66
|
#
|
@@ -49,129 +77,24 @@ module ROM
|
|
49
77
|
}
|
50
78
|
end
|
51
79
|
|
52
|
-
#
|
53
|
-
#
|
54
|
-
# Subsequent mapping definitions will be composed together
|
55
|
-
# and applied in the order they way defined
|
56
|
-
#
|
57
|
-
# @example Transformation DSL
|
58
|
-
# class NewUser < ROM::Changeset::Create
|
59
|
-
# map do
|
60
|
-
# unwrap :address, prefix: true
|
61
|
-
# end
|
62
|
-
# end
|
63
|
-
#
|
64
|
-
# @example Using custom block
|
65
|
-
# class NewUser < ROM::Changeset::Create
|
66
|
-
# map do |tuple|
|
67
|
-
# tuple.merge(created_at: Time.now)
|
68
|
-
# end
|
69
|
-
# end
|
70
|
-
#
|
71
|
-
# @example Multiple mappings (executed in the order of definition)
|
72
|
-
# class NewUser < ROM::Changeset::Create
|
73
|
-
# map do
|
74
|
-
# unwrap :address, prefix: true
|
75
|
-
# end
|
80
|
+
# Return a new changeset with updated options
|
76
81
|
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
82
|
+
# @example
|
83
|
+
# class NewUser < ROM::Changeset::Create[:users]
|
84
|
+
# option :token_generator, reader: true
|
80
85
|
# end
|
81
86
|
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
# @api public
|
85
|
-
def self.map(&block)
|
86
|
-
if block.arity.zero?
|
87
|
-
pipes << Class.new(Pipe, &block).new
|
88
|
-
else
|
89
|
-
pipes << Pipe.new(block)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
# Build default pipe object
|
94
|
-
#
|
95
|
-
# This can be overridden in a custom changeset subclass
|
96
|
-
#
|
97
|
-
# @return [Pipe]
|
98
|
-
def self.default_pipe(context)
|
99
|
-
pipes.size > 0 ? pipes.map { |p| p.bind(context) }.reduce(:>>) : Pipe.new
|
100
|
-
end
|
101
|
-
|
102
|
-
# @api private
|
103
|
-
def self.inherited(klass)
|
104
|
-
return if klass == ROM::Changeset
|
105
|
-
super
|
106
|
-
klass.instance_variable_set(:@__pipes__, pipes ? pipes.dup : [])
|
107
|
-
end
|
108
|
-
|
109
|
-
# @api private
|
110
|
-
def self.pipes
|
111
|
-
@__pipes__
|
112
|
-
end
|
113
|
-
|
114
|
-
# Pipe changeset's data using custom steps define on the pipe
|
115
|
-
#
|
116
|
-
# @param *steps [Array<Symbol>] A list of mapping steps
|
117
|
-
#
|
118
|
-
# @return [Changeset]
|
119
|
-
#
|
120
|
-
# @api public
|
121
|
-
def map(*steps, &block)
|
122
|
-
if block
|
123
|
-
__data__.map { |*args| yield(*args) }
|
124
|
-
else
|
125
|
-
with(pipe: steps.reduce(pipe) { |a, e| a >> pipe[e] })
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Coerce changeset to a hash
|
130
|
-
#
|
131
|
-
# This will send the data through the pipe
|
132
|
-
#
|
133
|
-
# @return [Hash]
|
134
|
-
#
|
135
|
-
# @api public
|
136
|
-
def to_h
|
137
|
-
pipe.call(__data__)
|
138
|
-
end
|
139
|
-
alias_method :to_hash, :to_h
|
140
|
-
|
141
|
-
# Coerce changeset to an array
|
142
|
-
#
|
143
|
-
# This will send the data through the pipe
|
144
|
-
#
|
145
|
-
# @return [Array]
|
146
|
-
#
|
147
|
-
# @api public
|
148
|
-
def to_a
|
149
|
-
result == :one ? [to_h] : __data__.map { |element| pipe.call(element) }
|
150
|
-
end
|
151
|
-
alias_method :to_ary, :to_a
|
152
|
-
|
153
|
-
# Return a new changeset with updated options
|
87
|
+
# changeset = user_repo.changeset(NewUser).with(token_generator: my_token_gen)
|
154
88
|
#
|
155
89
|
# @param [Hash] new_options The new options
|
156
90
|
#
|
157
91
|
# @return [Changeset]
|
158
92
|
#
|
159
|
-
# @api
|
93
|
+
# @api public
|
160
94
|
def with(new_options)
|
161
95
|
self.class.new(relation, options.merge(new_options))
|
162
96
|
end
|
163
97
|
|
164
|
-
# Return changeset with data
|
165
|
-
#
|
166
|
-
# @param [Hash] data
|
167
|
-
#
|
168
|
-
# @return [Changeset]
|
169
|
-
#
|
170
|
-
# @api public
|
171
|
-
def data(data)
|
172
|
-
with(__data__: data)
|
173
|
-
end
|
174
|
-
|
175
98
|
# Persist changeset
|
176
99
|
#
|
177
100
|
# @example
|
@@ -186,39 +109,36 @@ module ROM
|
|
186
109
|
command.call
|
187
110
|
end
|
188
111
|
|
189
|
-
# Return
|
112
|
+
# Return string representation of the changeset
|
190
113
|
#
|
191
|
-
# @return [
|
114
|
+
# @return [String]
|
192
115
|
#
|
193
|
-
# @api
|
194
|
-
def
|
195
|
-
|
116
|
+
# @api public
|
117
|
+
def inspect
|
118
|
+
%(#<#{self.class} relation=#{relation.name.inspect}>)
|
196
119
|
end
|
197
120
|
|
198
|
-
|
199
|
-
|
121
|
+
# Return a command for this changeset
|
122
|
+
#
|
123
|
+
# @return [ROM::Command]
|
124
|
+
#
|
200
125
|
# @api private
|
201
|
-
def
|
202
|
-
|
126
|
+
def command
|
127
|
+
command_compiler.(command_type, relation_identifier, DEFAULT_COMMAND_OPTS).new(relation)
|
203
128
|
end
|
204
129
|
|
130
|
+
private
|
131
|
+
|
205
132
|
# @api private
|
206
|
-
def
|
207
|
-
|
208
|
-
response = __data__.__send__(meth, *args, &block)
|
209
|
-
|
210
|
-
if response.is_a?(__data__.class)
|
211
|
-
with(__data__: response)
|
212
|
-
else
|
213
|
-
response
|
214
|
-
end
|
215
|
-
else
|
216
|
-
super
|
217
|
-
end
|
133
|
+
def relation_identifier
|
134
|
+
relation.name.relation
|
218
135
|
end
|
219
136
|
end
|
220
137
|
end
|
221
138
|
|
139
|
+
require 'rom/repository/changeset/stateful'
|
140
|
+
require 'rom/repository/changeset/associated'
|
141
|
+
|
222
142
|
require 'rom/repository/changeset/create'
|
223
143
|
require 'rom/repository/changeset/update'
|
224
144
|
require 'rom/repository/changeset/delete'
|
@@ -38,7 +38,7 @@ module ROM
|
|
38
38
|
# Define which relations your repository is going to use
|
39
39
|
#
|
40
40
|
# @example
|
41
|
-
# class MyRepo < ROM::Repository
|
41
|
+
# class MyRepo < ROM::Repository
|
42
42
|
# relations :users, :tasks
|
43
43
|
# end
|
44
44
|
#
|
@@ -83,7 +83,7 @@ module ROM
|
|
83
83
|
# commands :create, mapper: :my_custom_mapper
|
84
84
|
# end
|
85
85
|
#
|
86
|
-
# @param
|
86
|
+
# @param [Array<Symbol>] names A list of command names
|
87
87
|
# @option :mapper [Symbol] An optional mapper identifier
|
88
88
|
# @option :use [Symbol] An optional command plugin identifier
|
89
89
|
#
|
@@ -113,7 +113,7 @@ module ROM
|
|
113
113
|
# @api private
|
114
114
|
def define_command_method(type, **opts)
|
115
115
|
define_method(type) do |input|
|
116
|
-
if input.
|
116
|
+
if input.respond_to?(:commit)
|
117
117
|
map_tuple(input.relation, input.commit)
|
118
118
|
else
|
119
119
|
command(type => self.class.root, **opts).call(input)
|
@@ -131,7 +131,7 @@ module ROM
|
|
131
131
|
|
132
132
|
changeset = input.first
|
133
133
|
|
134
|
-
if changeset.
|
134
|
+
if changeset.respond_to?(:commit)
|
135
135
|
map_tuple(changeset.relation, changeset.commit)
|
136
136
|
else
|
137
137
|
command(type => self.class.root, **opts)
|
@@ -26,13 +26,15 @@ module ROM
|
|
26
26
|
# in repositories. It might be worth looking into removing this requirement
|
27
27
|
# from rom core Command::Graph API.
|
28
28
|
#
|
29
|
-
# @
|
30
|
-
# @param type [Symbol] The type of command
|
31
|
-
# @param adapter [Symbol] The adapter identifier
|
32
|
-
# @param ast [Array] The AST representation of a relation
|
33
|
-
# @param plugins [Array<Symbol>] A list of optional command plugins that should be used
|
29
|
+
# @overload [](container, type, adapter, ast, plugins, options)
|
34
30
|
#
|
35
|
-
#
|
31
|
+
# @param container [ROM::Container] container where relations are stored
|
32
|
+
# @param type [Symbol] The type of command
|
33
|
+
# @param adapter [Symbol] The adapter identifier
|
34
|
+
# @param ast [Array] The AST representation of a relation
|
35
|
+
# @param plugins [Array<Symbol>] A list of optional command plugins that should be used
|
36
|
+
#
|
37
|
+
# @return [Command, CommandProxy]
|
36
38
|
#
|
37
39
|
# @api private
|
38
40
|
def self.[](*args)
|
@@ -112,24 +114,24 @@ module ROM
|
|
112
114
|
name, meta, header = node
|
113
115
|
other = visit(header, name)
|
114
116
|
|
115
|
-
mapping =
|
116
|
-
if meta[:combine_type] == :many
|
117
|
-
name
|
118
|
-
else
|
119
|
-
{ Dry::Core::Inflector.singularize(name).to_sym => name }
|
120
|
-
end
|
121
|
-
|
122
117
|
if type
|
123
118
|
register_command(name, type, meta, parent_relation)
|
124
119
|
|
120
|
+
mapping =
|
121
|
+
if meta[:combine_type] == :many
|
122
|
+
name
|
123
|
+
else
|
124
|
+
{ Dry::Core::Inflector.singularize(name).to_sym => name }
|
125
|
+
end
|
126
|
+
|
125
127
|
if other.size > 0
|
126
128
|
[mapping, [type, other]]
|
127
129
|
else
|
128
130
|
[mapping, type]
|
129
131
|
end
|
130
132
|
else
|
131
|
-
registry[name][id] = container.commands[name][id]
|
132
|
-
[
|
133
|
+
registry[name][id] = container.commands[name][id]
|
134
|
+
[name, id]
|
133
135
|
end
|
134
136
|
end
|
135
137
|
|
@@ -48,8 +48,6 @@ module ROM
|
|
48
48
|
# @option :many [Hash] Sets options for "has-many" type of association
|
49
49
|
# @option :one [Hash] Sets options for "has-one/belongs-to" type of association
|
50
50
|
#
|
51
|
-
# @param [Hash] options
|
52
|
-
#
|
53
51
|
# @return [RelationProxy]
|
54
52
|
#
|
55
53
|
# @api public
|
@@ -190,8 +188,9 @@ module ROM
|
|
190
188
|
# used to get the keys. Otherwise we fall back to using default keys based
|
191
189
|
# on naming conventions.
|
192
190
|
#
|
193
|
-
# @param [
|
194
|
-
# @param [
|
191
|
+
# @param [Relation::Name] source The source relation name
|
192
|
+
# @param [Relation::Name] target The target relation name
|
193
|
+
# @param [Symbol] type The association type, can be either :parent or :children
|
195
194
|
#
|
196
195
|
# @return [Hash<Symbol=>Symbol>]
|
197
196
|
#
|
@@ -52,8 +52,21 @@ module ROM
|
|
52
52
|
|
53
53
|
# Maps the wrapped relation with other mappers available in the registry
|
54
54
|
#
|
55
|
-
# @
|
56
|
-
#
|
55
|
+
# @overload map_with(model)
|
56
|
+
# Map tuples to the provided custom model class
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# users.as(MyUserModel)
|
60
|
+
#
|
61
|
+
# @param [Class>] model Your custom model class
|
62
|
+
#
|
63
|
+
# @overload map_with(*mappers)
|
64
|
+
# Map tuples using registered mappers
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# users.as(MyUserModel)
|
68
|
+
#
|
69
|
+
# @param [Array<Symbol>] mappers A list of mapper identifiers
|
57
70
|
#
|
58
71
|
# @return [RelationProxy] A new relation proxy with pipelined relation
|
59
72
|
#
|
@@ -67,6 +80,15 @@ module ROM
|
|
67
80
|
end
|
68
81
|
alias_method :as, :map_with
|
69
82
|
|
83
|
+
# Return a string representation of this relation proxy
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
#
|
87
|
+
# @api public
|
88
|
+
def inspect
|
89
|
+
%(#<#{relation.class} name=#{name} dataset=#{dataset.inspect}>)
|
90
|
+
end
|
91
|
+
|
70
92
|
# Infers a mapper for the wrapped relation
|
71
93
|
#
|
72
94
|
# @return [ROM::Mapper]
|
@@ -136,11 +158,6 @@ module ROM
|
|
136
158
|
relation.respond_to?(meth) || super
|
137
159
|
end
|
138
160
|
|
139
|
-
# @api public
|
140
|
-
def inspect
|
141
|
-
%(#<#{relation.class} name=#{name} dataset=#{dataset.inspect}>)
|
142
|
-
end
|
143
|
-
|
144
161
|
private
|
145
162
|
|
146
163
|
# @api private
|
data/lib/rom/repository/root.rb
CHANGED
@@ -31,6 +31,19 @@ module ROM
|
|
31
31
|
class Root < Repository
|
32
32
|
extend Dry::Core::ClassAttributes
|
33
33
|
|
34
|
+
# @!method self.root
|
35
|
+
# Get or set repository root relation identifier
|
36
|
+
#
|
37
|
+
# This method is automatically used when you define a class using
|
38
|
+
# MyRepo[:rel_identifier] shortcut
|
39
|
+
#
|
40
|
+
# @overload root
|
41
|
+
# Return root relation identifier
|
42
|
+
# @return [Symbol]
|
43
|
+
#
|
44
|
+
# @overload root(identifier)
|
45
|
+
# Set root relation identifier
|
46
|
+
# @return [Symbol]
|
34
47
|
defines :root
|
35
48
|
|
36
49
|
# @!attribute [r] root
|
@@ -131,7 +144,7 @@ module ROM
|
|
131
144
|
#
|
132
145
|
# @return [Changeset]
|
133
146
|
#
|
134
|
-
# @
|
147
|
+
# @see Repository#changeset
|
135
148
|
#
|
136
149
|
# @api public
|
137
150
|
def changeset(*args)
|