rom-repository 1.0.0.beta3 → 1.0.0.rc1
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/.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)
|