graphiti-activegraph 1.3.1 → 1.3.3

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/specs.yml +58 -59
  3. data/.gitignore +59 -59
  4. data/.hound.yml +4 -0
  5. data/.rspec +1 -1
  6. data/.rubocop.yml +6 -0
  7. data/CHANGELOG.md +71 -54
  8. data/CHANGELOG_PRE_1.0.0.md +70 -70
  9. data/Gemfile +3 -3
  10. data/LICENSE.txt +21 -21
  11. data/README.md +130 -130
  12. data/docs/deserializer.md +40 -40
  13. data/graphiti-activegraph.gemspec +35 -34
  14. data/lib/graphiti/active_graph/adapters/active_graph/function_sideload.rb +7 -7
  15. data/lib/graphiti/active_graph/adapters/active_graph/has_many_sideload.rb +7 -7
  16. data/lib/graphiti/active_graph/adapters/active_graph/has_one_sideload.rb +7 -7
  17. data/lib/graphiti/active_graph/adapters/active_graph/polymorphic_belongs_to.rb +11 -11
  18. data/lib/graphiti/active_graph/adapters/active_graph/sideload.rb +26 -26
  19. data/lib/graphiti/active_graph/adapters/active_graph.rb +183 -183
  20. data/lib/graphiti/active_graph/concerns/path_relationships.rb +44 -44
  21. data/lib/graphiti/active_graph/concerns/relationships.rb +15 -15
  22. data/lib/graphiti/active_graph/deserializer.rb +138 -138
  23. data/lib/graphiti/active_graph/extensions/context.rb +17 -17
  24. data/lib/graphiti/active_graph/extensions/grouping/params.rb +101 -52
  25. data/lib/graphiti/active_graph/extensions/query_dsl/performer.rb +38 -38
  26. data/lib/graphiti/active_graph/extensions/query_dsl/query_generator.rb +20 -20
  27. data/lib/graphiti/active_graph/extensions/query_params.rb +27 -27
  28. data/lib/graphiti/active_graph/extensions/resources/authorizationable.rb +29 -29
  29. data/lib/graphiti/active_graph/extensions/resources/payload_combinable.rb +24 -24
  30. data/lib/graphiti/active_graph/extensions/resources/preloadable.rb +19 -19
  31. data/lib/graphiti/active_graph/extensions/resources/rel.rb +19 -19
  32. data/lib/graphiti/active_graph/jsonapi_ext/include_directive.rb +66 -66
  33. data/lib/graphiti/active_graph/jsonapi_ext/serializable/resource_ext.rb +8 -8
  34. data/lib/graphiti/active_graph/query.rb +76 -76
  35. data/lib/graphiti/active_graph/request_validators/validator.rb +9 -9
  36. data/lib/graphiti/active_graph/resource.rb +103 -103
  37. data/lib/graphiti/active_graph/resource_proxy.rb +86 -86
  38. data/lib/graphiti/active_graph/resources/interface.rb +14 -14
  39. data/lib/graphiti/active_graph/resources/persistence.rb +25 -25
  40. data/lib/graphiti/active_graph/runner.rb +39 -39
  41. data/lib/graphiti/active_graph/scope.rb +28 -28
  42. data/lib/graphiti/active_graph/scoping/association_eager_load.rb +35 -34
  43. data/lib/graphiti/active_graph/scoping/filter.rb +49 -49
  44. data/lib/graphiti/active_graph/scoping/filterable.rb +12 -12
  45. data/lib/graphiti/active_graph/scoping/include.rb +48 -48
  46. data/lib/graphiti/active_graph/scoping/internal/extra_field_normalizer.rb +76 -76
  47. data/lib/graphiti/active_graph/scoping/internal/include_normalizer.rb +82 -82
  48. data/lib/graphiti/active_graph/scoping/internal/path_descriptor.rb +94 -94
  49. data/lib/graphiti/active_graph/scoping/internal/sort_normalizer.rb +54 -54
  50. data/lib/graphiti/active_graph/scoping/internal/sorting_aliases.rb +35 -35
  51. data/lib/graphiti/active_graph/scoping/internal/sparse_fields_eagerloading.rb +28 -28
  52. data/lib/graphiti/active_graph/serializer.rb +15 -15
  53. data/lib/graphiti/active_graph/sideload_resolve.rb +119 -119
  54. data/lib/graphiti/active_graph/util/parsers/rel_chain.rb +27 -27
  55. data/lib/graphiti/active_graph/util/relationship_payload.rb +33 -33
  56. data/lib/graphiti/active_graph/util/serializer_attribute.rb +17 -17
  57. data/lib/graphiti/active_graph/util/serializer_relationship.rb +28 -28
  58. data/lib/graphiti/active_graph/util/transformers/relation_param.rb +56 -56
  59. data/lib/graphiti/active_graph/version.rb +5 -5
  60. data/lib/graphiti/sidepost_configuration.rb +9 -9
  61. data/lib/graphiti-activegraph.rb +43 -43
  62. metadata +21 -5
@@ -1,49 +1,49 @@
1
- module Graphiti::ActiveGraph
2
- module Scoping
3
- module Filter
4
- include Filterable
5
- include Internal::SortingAliases
6
- include Extensions::QueryDsl::Performer
7
-
8
- attr_reader :scope
9
-
10
- def apply
11
- super
12
- apply_query_dsl
13
- end
14
-
15
- def each_filter
16
- filter_param.each_pair do |param_name, param_value|
17
- filter = find_filter!(param_name)
18
-
19
- normalize_param(filter, param_value).each do |operator, value|
20
- operator = operator.to_s.gsub("!", "not_").to_sym
21
-
22
- # dynamic filters errors for validating and typecasting value below
23
- # so they are skipped here without validation or typecast
24
- filter_map = filter.values[0]
25
- if filter_map[:dynamic_filter]
26
- yield filter, operator, value
27
- next
28
- end
29
- validate_operator(filter, operator)
30
-
31
- type = ::Graphiti::Types[filter_map[:type]]
32
- unless type[:canonical_name] == :hash || !value.is_a?(String)
33
- value = parse_string_value(filter_map, value)
34
- end
35
-
36
- check_deny_empty_filters!(resource, filter, value)
37
- value = parse_string_null(filter_map, value)
38
- validate_singular(resource, filter, value)
39
- value = coerce_types(filter_map, param_name.to_sym, value)
40
- validate_allowlist(resource, filter, value)
41
- validate_denylist(resource, filter, value)
42
- value = value[0] if filter_map[:single]
43
- yield filter, operator, value
44
- end
45
- end
46
- end
47
- end
48
- end
49
- end
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Filter
4
+ include Filterable
5
+ include Internal::SortingAliases
6
+ include Extensions::QueryDsl::Performer
7
+
8
+ attr_reader :scope
9
+
10
+ def apply
11
+ super
12
+ apply_query_dsl
13
+ end
14
+
15
+ def each_filter
16
+ filter_param.each_pair do |param_name, param_value|
17
+ filter = find_filter!(param_name)
18
+
19
+ normalize_param(filter, param_value).each do |operator, value|
20
+ operator = operator.to_s.gsub("!", "not_").to_sym
21
+
22
+ # dynamic filters errors for validating and typecasting value below
23
+ # so they are skipped here without validation or typecast
24
+ filter_map = filter.values[0]
25
+ if filter_map[:dynamic_filter]
26
+ yield filter, operator, value
27
+ next
28
+ end
29
+ validate_operator(filter, operator)
30
+
31
+ type = ::Graphiti::Types[filter_map[:type]]
32
+ unless type[:canonical_name] == :hash || !value.is_a?(String)
33
+ value = parse_string_value(filter_map, value)
34
+ end
35
+
36
+ check_deny_empty_filters!(resource, filter, value)
37
+ value = parse_string_null(filter_map, value)
38
+ validate_singular(resource, filter, value)
39
+ value = coerce_types(filter_map, param_name.to_sym, value)
40
+ validate_allowlist(resource, filter, value)
41
+ validate_denylist(resource, filter, value)
42
+ value = value[0] if filter_map[:single]
43
+ yield filter, operator, value
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,12 +1,12 @@
1
- module Graphiti::ActiveGraph
2
- module Scoping
3
- module Filterable
4
- def find_filter!(name)
5
- val = resource.filters[name] || {
6
- operators: {}, type: :string, single: false, dynamic_filter: true
7
- }
8
- {name => val}
9
- end
10
- end
11
- end
12
- end
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Filterable
4
+ def find_filter!(name)
5
+ val = resource.filters[name] || {
6
+ operators: {}, type: :string, single: false, dynamic_filter: true
7
+ }
8
+ {name => val}
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,48 +1,48 @@
1
- module Graphiti::ActiveGraph
2
- module Scoping
3
- # Handles sideloading via scoping instead of sideloading query as in original jsonapi_suite
4
- # This avoids extra queries for fetching sideload
5
- class Include < Graphiti::Scoping::Base
6
- include Internal::SortingAliases
7
-
8
- def custom_scope
9
- nil
10
- end
11
-
12
- def apply_standard_scope
13
- return scope if normalized_includes.empty? && extra_fields_includes.empty?
14
-
15
- self.scope = resource.handle_includes(scope, normalized_includes, normalized_sorts,
16
- extra_fields_includes:, with_vars: with_vars_for_sort, paginate: paginate?)
17
- end
18
-
19
- private
20
-
21
- attr_accessor :scope
22
-
23
- def query
24
- @opts[:query_obj]
25
- end
26
-
27
- def extra_fields_includes
28
- @extra_fields_includes ||= Internal::ExtraFieldNormalizer.new(@query_hash[:extra_fields]).normalize(resource, normalized_includes)
29
- end
30
-
31
- def paginate?
32
- Graphiti::Scoping::Paginate.new(@resource, @query_hash, scope, @opts).apply?
33
- end
34
-
35
- def normalized_sorts
36
- Internal::SortNormalizer.new(scope).normalize(normalized_includes, query.sorts, query.deep_sort)
37
- end
38
-
39
- def include_normalizer
40
- Internal::IncludeNormalizer
41
- end
42
-
43
- def normalized_includes
44
- @normalized_includes ||= include_normalizer.new(resource.class, scope, query_hash[:fields]).normalize(query.include_hash)
45
- end
46
- end
47
- end
48
- end
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ # Handles sideloading via scoping instead of sideloading query as in original jsonapi_suite
4
+ # This avoids extra queries for fetching sideload
5
+ class Include < Graphiti::Scoping::Base
6
+ include Internal::SortingAliases
7
+
8
+ def custom_scope
9
+ nil
10
+ end
11
+
12
+ def apply_standard_scope
13
+ return scope if normalized_includes.empty? && extra_fields_includes.empty?
14
+
15
+ self.scope = resource.handle_includes(scope, normalized_includes, normalized_sorts,
16
+ extra_fields_includes:, with_vars: with_vars_for_sort, paginate: paginate?)
17
+ end
18
+
19
+ private
20
+
21
+ attr_accessor :scope
22
+
23
+ def query
24
+ @opts[:query_obj]
25
+ end
26
+
27
+ def extra_fields_includes
28
+ @extra_fields_includes ||= Internal::ExtraFieldNormalizer.new(@query_hash[:extra_fields]).normalize(resource, normalized_includes)
29
+ end
30
+
31
+ def paginate?
32
+ Graphiti::Scoping::Paginate.new(@resource, @query_hash, scope, @opts).apply?
33
+ end
34
+
35
+ def normalized_sorts
36
+ Internal::SortNormalizer.new(scope).normalize(normalized_includes, query.sorts, query.deep_sort)
37
+ end
38
+
39
+ def include_normalizer
40
+ Internal::IncludeNormalizer
41
+ end
42
+
43
+ def normalized_includes
44
+ @normalized_includes ||= include_normalizer.new(resource.class, scope, query_hash[:fields]).normalize(query.include_hash)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,76 +1,76 @@
1
- module Graphiti::ActiveGraph
2
- module Scoping
3
- module Internal
4
- class ExtraFieldNormalizer
5
-
6
- def initialize(extra_fields)
7
- @extra_fields = extra_fields
8
- @extra_includes = []
9
- end
10
-
11
- def normalize(resource, normalized_includes)
12
- return [] if @extra_fields.blank?
13
-
14
- process_extra_fields_for_assoc(resource, [], '')
15
- collect_extra_field_paths(resource, normalized_includes) unless normalized_includes.blank?
16
- @extra_includes.uniq
17
- end
18
-
19
- private
20
-
21
- def collect_extra_field_paths(resource, normalized_includes, parent_path = [])
22
- normalized_includes.each do |assoc, nested_assoc|
23
- assoc_resource = fetch_assoc_resource(resource, assoc)
24
- next unless assoc_resource
25
-
26
- process_extra_fields_for_assoc(assoc_resource, parent_path, assoc)
27
- collect_extra_field_paths(assoc_resource, nested_assoc, parent_path + [assoc.to_s]) unless nested_assoc.empty?
28
- end
29
- end
30
-
31
- def fetch_assoc_resource(resource, assoc)
32
- rel_name = Util::Transformers::RelationParam.new(assoc).rel_name_sym
33
- resource.class&.sideload_resource_class(rel_name)&.new
34
- end
35
-
36
- def process_extra_fields_for_assoc(assoc_resource, parent_path, assoc)
37
- return unless @extra_fields.key?(assoc_resource.type)
38
-
39
- Array(@extra_fields[assoc_resource.type]).each do |extra_field|
40
- add_preload_paths_for_extra_field(extra_field_config(assoc_resource, extra_field), parent_path, assoc)
41
- end
42
- end
43
-
44
- def extra_field_config(assoc_resource, extra_field)
45
- assoc_resource.class&.config&.dig(:extra_attributes, extra_field)
46
- end
47
-
48
- def add_preload_paths_for_extra_field(config, parent_path, assoc)
49
- return unless config && config[:preload].present?
50
-
51
- flatten_preload_hash(config[:preload]).each do |preload|
52
- @extra_includes << construct_preload_path(parent_path, assoc, preload)
53
- end
54
- end
55
-
56
- def flatten_preload_hash(preload, prefix = [])
57
- case preload
58
- when Hash
59
- preload.flat_map { |k, v| flatten_preload_hash(v, prefix + [k.to_s]) }
60
- when Array
61
- preload.flat_map { |v| flatten_preload_hash(v, prefix) }
62
- else
63
- value = preload.to_s
64
- return [] if value.empty?
65
-
66
- [(prefix + [value]).join('.')]
67
- end
68
- end
69
-
70
- def construct_preload_path(parent_path, assoc, preload)
71
- (parent_path + [assoc.to_s, preload.to_s]).compact_blank.join('.')
72
- end
73
- end
74
- end
75
- end
76
- end
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Internal
4
+ class ExtraFieldNormalizer
5
+
6
+ def initialize(extra_fields)
7
+ @extra_fields = extra_fields
8
+ @extra_includes = []
9
+ end
10
+
11
+ def normalize(resource, normalized_includes)
12
+ return [] if @extra_fields.blank?
13
+
14
+ process_extra_fields_for_assoc(resource, [], '')
15
+ collect_extra_field_paths(resource, normalized_includes) unless normalized_includes.blank?
16
+ @extra_includes.uniq
17
+ end
18
+
19
+ private
20
+
21
+ def collect_extra_field_paths(resource, normalized_includes, parent_path = [])
22
+ normalized_includes.each do |assoc, nested_assoc|
23
+ assoc_resource = fetch_assoc_resource(resource, assoc)
24
+ next unless assoc_resource
25
+
26
+ process_extra_fields_for_assoc(assoc_resource, parent_path, assoc)
27
+ collect_extra_field_paths(assoc_resource, nested_assoc, parent_path + [assoc.to_s]) unless nested_assoc.empty?
28
+ end
29
+ end
30
+
31
+ def fetch_assoc_resource(resource, assoc)
32
+ rel_name = Util::Transformers::RelationParam.new(assoc).rel_name_sym
33
+ resource.class&.sideload_resource_class(rel_name)&.new
34
+ end
35
+
36
+ def process_extra_fields_for_assoc(assoc_resource, parent_path, assoc)
37
+ return unless @extra_fields.key?(assoc_resource.type)
38
+
39
+ Array(@extra_fields[assoc_resource.type]).each do |extra_field|
40
+ add_preload_paths_for_extra_field(extra_field_config(assoc_resource, extra_field), parent_path, assoc)
41
+ end
42
+ end
43
+
44
+ def extra_field_config(assoc_resource, extra_field)
45
+ assoc_resource.class&.config&.dig(:extra_attributes, extra_field)
46
+ end
47
+
48
+ def add_preload_paths_for_extra_field(config, parent_path, assoc)
49
+ return unless config && config[:preload].present?
50
+
51
+ flatten_preload_hash(config[:preload]).each do |preload|
52
+ @extra_includes << construct_preload_path(parent_path, assoc, preload)
53
+ end
54
+ end
55
+
56
+ def flatten_preload_hash(preload, prefix = [])
57
+ case preload
58
+ when Hash
59
+ preload.flat_map { |k, v| flatten_preload_hash(v, prefix + [k.to_s]) }
60
+ when Array
61
+ preload.flat_map { |v| flatten_preload_hash(v, prefix) }
62
+ else
63
+ value = preload.to_s
64
+ return [] if value.empty?
65
+
66
+ [(prefix + [value]).join('.')]
67
+ end
68
+ end
69
+
70
+ def construct_preload_path(parent_path, assoc, preload)
71
+ (parent_path + [assoc.to_s, preload.to_s]).compact_blank.join('.')
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,82 +1,82 @@
1
- module Graphiti::ActiveGraph
2
- module Scoping
3
- module Internal
4
- class IncludeNormalizer
5
- include SparseFieldsEagerloading
6
-
7
- def initialize(resource_class, scope, fields)
8
- @scope = scope
9
- @resource_class = resource_class
10
- @fields = fields
11
- end
12
-
13
- def normalize(include_hash)
14
- normalize_includes(@scope, include_hash, @resource_class)
15
- end
16
-
17
- private
18
-
19
- def normalize_includes(scope, include_hash, resource_class)
20
- includes_array = include_hash.map do |key, value|
21
- normalize_include(scope, key, value, resource_class)
22
- end
23
- add_relationships_from_sparse_fields(scope, includes_array)
24
- deep_merge_hashes(includes_array.compact).to_h
25
- end
26
-
27
- def deep_merge_hashes(includes_array)
28
- includes_array.each_with_object({}) do |(key, value), mapping|
29
- mapping[key] = mapping[key] ? mapping[key].deep_merge(value) : value
30
- end.to_a
31
- end
32
-
33
- def normalize_include(scope, key, value, resource_class)
34
- rel_name = rel_name_sym(key)
35
-
36
- if scope.associations.key?(rel_name)
37
- [key, normalize_includes(scope.send(rel_name), value, find_resource_class(resource_class, rel_name, scope))]
38
- elsif (custom_eagerload = resource_class&.custom_eagerload(rel_name))
39
- handle_custom_eagerload(scope, custom_eagerload)
40
- else
41
- include_for_rel(scope, rel_name, value, resource_class)
42
- end
43
- end
44
-
45
- def handle_custom_eagerload(_scope, custom_eagerload)
46
- JSONAPI::IncludeDirective.new(custom_eagerload).to_hash
47
- end
48
-
49
- def include_for_rel(scope, key, value, resource_class)
50
- return unless association = PathDescriptor.association_for_relationship(scope.associations, rel_name: key.to_s)
51
-
52
- limit_part = Graphiti::ActiveGraph::Util::Transformers::RelationParam.new(value.keys.first).rel_limit
53
- association_name = "#{limit_part}#{association.first}".to_sym
54
- normalize_include(scope, association_name, next_non_rel_value(value), resource_class_by_rel(resource_class, association, key, scope))
55
- end
56
-
57
- def find_resource_class(resource_class, rel_name, scope)
58
- target_class_name = scope.associations[rel_name]&.target_class&.model_name
59
-
60
- resource_class&.sideload_resource_class(rel_name) ||
61
- resource_class&.sideload_resource_class(target_class_name&.singular&.to_sym)
62
- end
63
-
64
- def resource_class_by_rel(resource_class, association, key, scope)
65
- # in case of rel resource, for finding custom_eagerload defination
66
- # if current resourceClass defination has direct association defined with opposite node of relResource
67
- # then use current resourceClass, (giving direct resourceClass more preference than relResourceClass)
68
- # else use relResourceClass
69
- find_resource_class(resource_class, association.first, scope) ? resource_class : resource_class&.sideload_resource_class(rel_name_sym(key))
70
- end
71
-
72
- def rel_name_sym(key)
73
- Graphiti::ActiveGraph::Util::Transformers::RelationParam.new(key).rel_name_sym
74
- end
75
-
76
- def next_non_rel_value(value)
77
- value.values.first || {}
78
- end
79
- end
80
- end
81
- end
82
- end
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Internal
4
+ class IncludeNormalizer
5
+ include SparseFieldsEagerloading
6
+
7
+ def initialize(resource_class, scope, fields)
8
+ @scope = scope
9
+ @resource_class = resource_class
10
+ @fields = fields
11
+ end
12
+
13
+ def normalize(include_hash)
14
+ normalize_includes(@scope, include_hash, @resource_class)
15
+ end
16
+
17
+ private
18
+
19
+ def normalize_includes(scope, include_hash, resource_class)
20
+ includes_array = include_hash.map do |key, value|
21
+ normalize_include(scope, key, value, resource_class)
22
+ end
23
+ add_relationships_from_sparse_fields(scope, includes_array)
24
+ deep_merge_hashes(includes_array.compact).to_h
25
+ end
26
+
27
+ def deep_merge_hashes(includes_array)
28
+ includes_array.each_with_object({}) do |(key, value), mapping|
29
+ mapping[key] = mapping[key] ? mapping[key].deep_merge(value) : value
30
+ end.to_a
31
+ end
32
+
33
+ def normalize_include(scope, key, value, resource_class)
34
+ rel_name = rel_name_sym(key)
35
+
36
+ if scope.associations.key?(rel_name)
37
+ [key, normalize_includes(scope.send(rel_name), value, find_resource_class(resource_class, rel_name, scope))]
38
+ elsif (custom_eagerload = resource_class&.custom_eagerload(rel_name))
39
+ handle_custom_eagerload(scope, custom_eagerload)
40
+ else
41
+ include_for_rel(scope, rel_name, value, resource_class)
42
+ end
43
+ end
44
+
45
+ def handle_custom_eagerload(_scope, custom_eagerload)
46
+ JSONAPI::IncludeDirective.new(custom_eagerload).to_hash
47
+ end
48
+
49
+ def include_for_rel(scope, key, value, resource_class)
50
+ return unless association = PathDescriptor.association_for_relationship(scope.associations, rel_name: key.to_s)
51
+
52
+ limit_part = Graphiti::ActiveGraph::Util::Transformers::RelationParam.new(value.keys.first).rel_limit
53
+ association_name = "#{limit_part}#{association.first}".to_sym
54
+ normalize_include(scope, association_name, next_non_rel_value(value), resource_class_by_rel(resource_class, association, key, scope))
55
+ end
56
+
57
+ def find_resource_class(resource_class, rel_name, scope)
58
+ target_class_name = scope.associations[rel_name]&.target_class&.model_name
59
+
60
+ resource_class&.sideload_resource_class(rel_name) ||
61
+ resource_class&.sideload_resource_class(target_class_name&.singular&.to_sym)
62
+ end
63
+
64
+ def resource_class_by_rel(resource_class, association, key, scope)
65
+ # in case of rel resource, for finding custom_eagerload defination
66
+ # if current resourceClass defination has direct association defined with opposite node of relResource
67
+ # then use current resourceClass, (giving direct resourceClass more preference than relResourceClass)
68
+ # else use relResourceClass
69
+ find_resource_class(resource_class, association.first, scope) ? resource_class : resource_class&.sideload_resource_class(rel_name_sym(key))
70
+ end
71
+
72
+ def rel_name_sym(key)
73
+ Graphiti::ActiveGraph::Util::Transformers::RelationParam.new(key).rel_name_sym
74
+ end
75
+
76
+ def next_non_rel_value(value)
77
+ value.values.first || {}
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end