rom-repository 1.4.0 → 2.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/CHANGELOG.md +14 -6
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +18 -1
- data/lib/rom-repository.rb +1 -2
- data/lib/rom/repository.rb +9 -216
- data/lib/rom/repository/class_interface.rb +16 -33
- data/lib/rom/repository/relation_reader.rb +46 -0
- data/lib/rom/repository/root.rb +3 -59
- data/lib/rom/repository/version.rb +1 -1
- metadata +9 -98
- data/.gitignore +0 -3
- data/.rspec +0 -3
- data/.travis.yml +0 -27
- data/.yardopts +0 -2
- data/Gemfile +0 -38
- data/Rakefile +0 -19
- data/lib/rom/open_struct.rb +0 -35
- data/lib/rom/repository/changeset.rb +0 -155
- data/lib/rom/repository/changeset/associated.rb +0 -100
- data/lib/rom/repository/changeset/create.rb +0 -16
- data/lib/rom/repository/changeset/delete.rb +0 -17
- data/lib/rom/repository/changeset/pipe.rb +0 -97
- data/lib/rom/repository/changeset/restricted.rb +0 -28
- data/lib/rom/repository/changeset/stateful.rb +0 -282
- data/lib/rom/repository/changeset/update.rb +0 -82
- data/lib/rom/repository/command_compiler.rb +0 -257
- data/lib/rom/repository/command_proxy.rb +0 -26
- data/lib/rom/repository/header_builder.rb +0 -65
- data/lib/rom/repository/mapper_builder.rb +0 -23
- data/lib/rom/repository/relation_proxy.rb +0 -337
- data/lib/rom/repository/relation_proxy/combine.rb +0 -320
- data/lib/rom/repository/relation_proxy/wrap.rb +0 -78
- data/lib/rom/repository/struct_builder.rb +0 -83
- data/lib/rom/struct.rb +0 -113
- data/log/.gitkeep +0 -0
- data/rom-repository.gemspec +0 -23
- data/spec/integration/changeset_spec.rb +0 -193
- data/spec/integration/command_macros_spec.rb +0 -191
- data/spec/integration/command_spec.rb +0 -228
- data/spec/integration/multi_adapter_spec.rb +0 -73
- data/spec/integration/repository/aggregate_spec.rb +0 -58
- data/spec/integration/repository_spec.rb +0 -406
- data/spec/integration/root_repository_spec.rb +0 -106
- data/spec/integration/typed_structs_spec.rb +0 -64
- data/spec/shared/database.rb +0 -79
- data/spec/shared/mappers.rb +0 -35
- data/spec/shared/models.rb +0 -41
- data/spec/shared/plugins.rb +0 -66
- data/spec/shared/relations.rb +0 -115
- data/spec/shared/repo.rb +0 -86
- data/spec/shared/seeds.rb +0 -30
- data/spec/shared/structs.rb +0 -140
- data/spec/spec_helper.rb +0 -83
- data/spec/support/mapper_registry.rb +0 -9
- data/spec/support/mutant.rb +0 -10
- data/spec/unit/changeset/associate_spec.rb +0 -120
- data/spec/unit/changeset/map_spec.rb +0 -111
- data/spec/unit/changeset_spec.rb +0 -186
- data/spec/unit/relation_proxy_spec.rb +0 -202
- data/spec/unit/repository/changeset_spec.rb +0 -197
- data/spec/unit/repository/inspect_spec.rb +0 -18
- data/spec/unit/repository/session_spec.rb +0 -251
- data/spec/unit/repository/transaction_spec.rb +0 -42
- data/spec/unit/session_spec.rb +0 -46
- data/spec/unit/struct_builder_spec.rb +0 -128
@@ -1,82 +0,0 @@
|
|
1
|
-
require 'rom/repository/changeset/restricted'
|
2
|
-
|
3
|
-
module ROM
|
4
|
-
class Changeset
|
5
|
-
# Changeset specialization for update commands
|
6
|
-
#
|
7
|
-
# Update changesets will only execute their commands when
|
8
|
-
# the data is different from the original tuple. Original tuple
|
9
|
-
# is fetched from changeset's relation using `by_pk` relation view.
|
10
|
-
# This means the underlying adapter must provide this view, or you
|
11
|
-
# you need to implement it yourself in your relations if you want to
|
12
|
-
# use Update changesets.
|
13
|
-
#
|
14
|
-
# @see Changeset::Stateful
|
15
|
-
#
|
16
|
-
# @api public
|
17
|
-
class Update < Stateful
|
18
|
-
include Restricted
|
19
|
-
|
20
|
-
command_type :update
|
21
|
-
|
22
|
-
# Commit update changeset if there's a diff
|
23
|
-
#
|
24
|
-
# This returns original tuple if there's no diff
|
25
|
-
#
|
26
|
-
# @return [Hash]
|
27
|
-
#
|
28
|
-
# @see Changeset#commit
|
29
|
-
#
|
30
|
-
# @api public
|
31
|
-
def commit
|
32
|
-
diff? ? super : original
|
33
|
-
end
|
34
|
-
|
35
|
-
# Return original tuple that this changeset may update
|
36
|
-
#
|
37
|
-
# @return [Hash]
|
38
|
-
#
|
39
|
-
# @api public
|
40
|
-
def original
|
41
|
-
@original ||= Hash(relation.one)
|
42
|
-
end
|
43
|
-
|
44
|
-
# Return true if there's a diff between original and changeset data
|
45
|
-
#
|
46
|
-
# @return [TrueClass, FalseClass]
|
47
|
-
#
|
48
|
-
# @api public
|
49
|
-
def diff?
|
50
|
-
! diff.empty?
|
51
|
-
end
|
52
|
-
|
53
|
-
# Return if there's no diff between the original and changeset data
|
54
|
-
#
|
55
|
-
# @return [TrueClass, FalseClass]
|
56
|
-
#
|
57
|
-
# @api public
|
58
|
-
def clean?
|
59
|
-
diff.empty?
|
60
|
-
end
|
61
|
-
|
62
|
-
# Calculate the diff between the original and changeset data
|
63
|
-
#
|
64
|
-
# @return [Hash]
|
65
|
-
#
|
66
|
-
# @api public
|
67
|
-
def diff
|
68
|
-
@diff ||=
|
69
|
-
begin
|
70
|
-
data = pipe.for_diff(__data__)
|
71
|
-
data_tuple = data.to_a
|
72
|
-
data_keys = data.keys & original.keys
|
73
|
-
|
74
|
-
new_tuple = data_tuple.to_a.select { |(k, _)| data_keys.include?(k) }
|
75
|
-
ori_tuple = original.to_a.select { |(k, _)| data_keys.include?(k) }
|
76
|
-
|
77
|
-
Hash[new_tuple - (new_tuple & ori_tuple)]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
@@ -1,257 +0,0 @@
|
|
1
|
-
require 'dry/core/inflector'
|
2
|
-
require 'dry/core/cache'
|
3
|
-
|
4
|
-
require 'rom/commands'
|
5
|
-
require 'rom/repository/command_proxy'
|
6
|
-
|
7
|
-
module ROM
|
8
|
-
class Repository
|
9
|
-
# Builds commands for relations.
|
10
|
-
#
|
11
|
-
# This class is used by repositories to automatically create commands for
|
12
|
-
# their relations. This is used both by `Repository#command` method and
|
13
|
-
# `commands` repository class macros.
|
14
|
-
#
|
15
|
-
# @api private
|
16
|
-
class CommandCompiler
|
17
|
-
extend Dry::Core::Cache
|
18
|
-
|
19
|
-
# Return a specific command type for a given adapter and relation AST
|
20
|
-
#
|
21
|
-
# This class holds its own registry where all generated commands are being
|
22
|
-
# stored
|
23
|
-
#
|
24
|
-
# CommandProxy is returned for complex command graphs as they expect root
|
25
|
-
# relation name to be present in the input, which we don't want to have
|
26
|
-
# in repositories. It might be worth looking into removing this requirement
|
27
|
-
# from rom core Command::Graph API.
|
28
|
-
#
|
29
|
-
# @overload [](container, type, adapter, ast, plugins, options)
|
30
|
-
#
|
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]
|
38
|
-
#
|
39
|
-
# @api private
|
40
|
-
def self.[](*args)
|
41
|
-
fetch_or_store(args.hash) do
|
42
|
-
container, type, adapter, ast, plugins, options = args
|
43
|
-
|
44
|
-
graph_opts = new(type, adapter, container, registry, plugins, options).visit(ast)
|
45
|
-
|
46
|
-
command = ROM::Commands::Graph.build(registry, graph_opts)
|
47
|
-
|
48
|
-
if command.graph?
|
49
|
-
CommandProxy.new(command)
|
50
|
-
elsif command.lazy?
|
51
|
-
command.unwrap
|
52
|
-
else
|
53
|
-
command
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# @api private
|
59
|
-
def self.registry
|
60
|
-
@__registry__ ||= Hash.new { |h, k| h[k] = {} }
|
61
|
-
end
|
62
|
-
|
63
|
-
# @!attribute [r] id
|
64
|
-
# @return [Symbol] The command type registry identifier
|
65
|
-
attr_reader :id
|
66
|
-
|
67
|
-
# @!attribute [r] adapter
|
68
|
-
# @return [Symbol] The adapter identifier ie :sql or :http
|
69
|
-
attr_reader :adapter
|
70
|
-
|
71
|
-
# @!attribute [r] container
|
72
|
-
# @return [ROM::Container] rom container with relations and gateways
|
73
|
-
attr_reader :container
|
74
|
-
|
75
|
-
# @!attribute [r] registry
|
76
|
-
# @return [Hash] local registry where commands will be stored during compilation
|
77
|
-
attr_reader :registry
|
78
|
-
|
79
|
-
# @!attribute [r] plugins
|
80
|
-
# @return [Array<Symbol>] a list of optional plugins that will be enabled for commands
|
81
|
-
attr_reader :plugins
|
82
|
-
|
83
|
-
# @!attribute [r] options
|
84
|
-
# @return [Hash] Additional command options
|
85
|
-
attr_reader :options
|
86
|
-
|
87
|
-
# @api private
|
88
|
-
def initialize(id, adapter, container, registry, plugins, options)
|
89
|
-
@id = id
|
90
|
-
@adapter = adapter
|
91
|
-
@registry = registry
|
92
|
-
@container = container
|
93
|
-
@plugins = Array(plugins)
|
94
|
-
@options = options
|
95
|
-
end
|
96
|
-
|
97
|
-
# @api private
|
98
|
-
def type
|
99
|
-
@_type ||= Commands.const_get(Dry::Core::Inflector.classify(id))[adapter]
|
100
|
-
rescue NameError
|
101
|
-
nil
|
102
|
-
end
|
103
|
-
|
104
|
-
# @api private
|
105
|
-
def visit(ast, *args)
|
106
|
-
name, node = ast
|
107
|
-
__send__(:"visit_#{name}", node, *args)
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
# @api private
|
113
|
-
def visit_relation(node, parent_relation = nil)
|
114
|
-
name, meta, header = node
|
115
|
-
other = visit(header, name)
|
116
|
-
|
117
|
-
if type
|
118
|
-
register_command(name, type, meta, parent_relation)
|
119
|
-
|
120
|
-
default_mapping =
|
121
|
-
if meta[:combine_type] == :many
|
122
|
-
name
|
123
|
-
else meta[:combine_type] == :one
|
124
|
-
{ Dry::Core::Inflector.singularize(name).to_sym => name }
|
125
|
-
end
|
126
|
-
|
127
|
-
mapping =
|
128
|
-
if parent_relation
|
129
|
-
associations = container.relations[parent_relation].associations
|
130
|
-
|
131
|
-
assoc =
|
132
|
-
if associations.key?(meta[:combine_name])
|
133
|
-
associations[meta[:combine_name]]
|
134
|
-
elsif associations.key?(name)
|
135
|
-
associations[name]
|
136
|
-
end
|
137
|
-
|
138
|
-
if assoc
|
139
|
-
{ assoc.target.key => assoc.target.dataset }
|
140
|
-
else
|
141
|
-
default_mapping
|
142
|
-
end
|
143
|
-
else
|
144
|
-
default_mapping
|
145
|
-
end
|
146
|
-
|
147
|
-
if other.size > 0
|
148
|
-
[mapping, [type, other]]
|
149
|
-
else
|
150
|
-
[mapping, type]
|
151
|
-
end
|
152
|
-
else
|
153
|
-
registry[name][id] = container.commands[name][id]
|
154
|
-
[name, id]
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# @api private
|
159
|
-
def visit_header(node, *args)
|
160
|
-
node.map { |n| visit(n, *args) }.compact
|
161
|
-
end
|
162
|
-
|
163
|
-
# @api private
|
164
|
-
def visit_attribute(*args)
|
165
|
-
nil
|
166
|
-
end
|
167
|
-
|
168
|
-
# Build a command object for a specific relation
|
169
|
-
#
|
170
|
-
# The command will be prepared for handling associations if it's a combined
|
171
|
-
# relation. Additional plugins will be enabled if they are configured for
|
172
|
-
# this compiler.
|
173
|
-
#
|
174
|
-
# @param [Symbol] rel_name A relation identifier from the container registry
|
175
|
-
# @param [Symbol] type The command type
|
176
|
-
# @param [Hash] meta Meta information from relation AST
|
177
|
-
# @param [Symbol] parent_relation Optional parent relation identifier
|
178
|
-
#
|
179
|
-
# @return [ROM::Command]
|
180
|
-
#
|
181
|
-
# @api private
|
182
|
-
def register_command(rel_name, type, meta, parent_relation = nil)
|
183
|
-
relation = container.relations[rel_name]
|
184
|
-
|
185
|
-
type.create_class(rel_name, type) do |klass|
|
186
|
-
klass.result(meta.fetch(:combine_type, result))
|
187
|
-
|
188
|
-
if meta[:combine_type]
|
189
|
-
setup_associates(klass, relation, meta, parent_relation)
|
190
|
-
end
|
191
|
-
|
192
|
-
finalize_command_class(klass, relation)
|
193
|
-
|
194
|
-
registry[rel_name][type] = klass.build(relation, input: relation.input_schema)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# Return default result type
|
199
|
-
#
|
200
|
-
# @return [Symbol]
|
201
|
-
#
|
202
|
-
# @api private
|
203
|
-
def result
|
204
|
-
options.fetch(:result, :one)
|
205
|
-
end
|
206
|
-
|
207
|
-
# Sets up `associates` plugin for a given command class and relation
|
208
|
-
#
|
209
|
-
# @param [Class] klass The command class
|
210
|
-
# @param [Relation] relation The relation for the command
|
211
|
-
#
|
212
|
-
# @api private
|
213
|
-
def setup_associates(klass, relation, meta, parent_relation)
|
214
|
-
assoc_name =
|
215
|
-
if relation.associations.key?(parent_relation)
|
216
|
-
parent_relation
|
217
|
-
else
|
218
|
-
singular_name = Dry::Core::Inflector.singularize(parent_relation).to_sym
|
219
|
-
singular_name if relation.associations.key?(singular_name)
|
220
|
-
end
|
221
|
-
|
222
|
-
if assoc_name
|
223
|
-
klass.associates(assoc_name)
|
224
|
-
else
|
225
|
-
keys = meta[:keys].invert.to_a.flatten
|
226
|
-
klass.associates(parent_relation, key: keys)
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
# Setup a command class for a specific relation
|
231
|
-
#
|
232
|
-
# Every gateway may provide custom command extensions via
|
233
|
-
# `Gateway#extend_command_class`. Furthermore, restrictible commands like
|
234
|
-
# `Update` or `Delete` will be extended with relation view methods, so things
|
235
|
-
# like `delete_user.by_id(1).call` becomes available.
|
236
|
-
#
|
237
|
-
# @param [Class] klass The command class
|
238
|
-
# @param [Relation] relation The command relation
|
239
|
-
#
|
240
|
-
# @return [Class]
|
241
|
-
#
|
242
|
-
# @api private
|
243
|
-
def finalize_command_class(klass, relation)
|
244
|
-
# TODO: this is a copy-paste from rom's FinalizeCommands, we are missing
|
245
|
-
# an interface!
|
246
|
-
gateway = container.gateways[relation.class.gateway]
|
247
|
-
gateway.extend_command_class(klass, relation.dataset)
|
248
|
-
|
249
|
-
klass.extend_for_relation(relation) if klass.restrictable
|
250
|
-
|
251
|
-
plugins.each do |plugin|
|
252
|
-
klass.use(plugin)
|
253
|
-
end
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'dry/core/inflector'
|
2
|
-
|
3
|
-
module ROM
|
4
|
-
class Repository
|
5
|
-
# TODO: look into making command graphs work without the root key in the input
|
6
|
-
# so that we can get rid of this wrapper
|
7
|
-
#
|
8
|
-
# @api private
|
9
|
-
class CommandProxy
|
10
|
-
attr_reader :command, :root
|
11
|
-
|
12
|
-
def initialize(command)
|
13
|
-
@command = command
|
14
|
-
@root = Dry::Core::Inflector.singularize(command.name.relation).to_sym
|
15
|
-
end
|
16
|
-
|
17
|
-
def call(input)
|
18
|
-
command.call(root => input)
|
19
|
-
end
|
20
|
-
|
21
|
-
def >>(other)
|
22
|
-
self.class.new(command >> other)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,65 +0,0 @@
|
|
1
|
-
require 'rom/header'
|
2
|
-
require 'rom/repository/struct_builder'
|
3
|
-
|
4
|
-
module ROM
|
5
|
-
class Repository
|
6
|
-
# @api private
|
7
|
-
class HeaderBuilder
|
8
|
-
attr_reader :struct_builder
|
9
|
-
|
10
|
-
def initialize(struct_namespace: nil, **options)
|
11
|
-
@struct_builder = StructBuilder.new(struct_namespace)
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(ast)
|
15
|
-
Header.coerce(*visit(ast))
|
16
|
-
end
|
17
|
-
alias_method :[], :call
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def visit(node)
|
22
|
-
name, node = node
|
23
|
-
__send__("visit_#{name}", node)
|
24
|
-
end
|
25
|
-
|
26
|
-
def visit_relation(node)
|
27
|
-
relation_name, meta, header = node
|
28
|
-
name = meta[:combine_name] || relation_name
|
29
|
-
|
30
|
-
model = meta.fetch(:model) do
|
31
|
-
if meta[:combine_name]
|
32
|
-
false
|
33
|
-
else
|
34
|
-
struct_builder[name, header]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
options = [visit(header), model: model]
|
39
|
-
|
40
|
-
if meta[:combine_type]
|
41
|
-
type = meta[:combine_type] == :many ? :array : :hash
|
42
|
-
keys = meta.fetch(:keys)
|
43
|
-
|
44
|
-
[name, combine: true, type: type, keys: keys, header: Header.coerce(*options)]
|
45
|
-
elsif meta[:wrap]
|
46
|
-
[name, wrap: true, type: :hash, header: Header.coerce(*options)]
|
47
|
-
else
|
48
|
-
options
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def visit_header(node)
|
53
|
-
node.map { |attribute| visit(attribute) }
|
54
|
-
end
|
55
|
-
|
56
|
-
def visit_attribute(attr)
|
57
|
-
if attr.wrapped?
|
58
|
-
[attr.name, from: attr.alias]
|
59
|
-
else
|
60
|
-
[attr.name]
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
require 'dry/core/cache'
|
2
|
-
require 'rom/mapper'
|
3
|
-
require 'rom/repository/header_builder'
|
4
|
-
|
5
|
-
module ROM
|
6
|
-
class Repository
|
7
|
-
# @api private
|
8
|
-
class MapperBuilder
|
9
|
-
extend Dry::Core::Cache
|
10
|
-
|
11
|
-
attr_reader :header_builder
|
12
|
-
|
13
|
-
def initialize(options = EMPTY_HASH)
|
14
|
-
@header_builder = HeaderBuilder.new(options)
|
15
|
-
end
|
16
|
-
|
17
|
-
def call(ast)
|
18
|
-
fetch_or_store(ast) { Mapper.build(header_builder[ast]) }
|
19
|
-
end
|
20
|
-
alias_method :[], :call
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|