graphiti-activegraph 1.0.0 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0928b60dbd846d64a29c21e84023b471b619e437261e1d8b77f840e5c633dd81'
4
- data.tar.gz: 69afd454fcf50c4ffa50930de69bea8d182c5e32681c9be2c9c33b676719f102
3
+ metadata.gz: c387f138fe40ab7a304c5d587a0281258e2bf4e8304d7e0f0967295adeeef6d7
4
+ data.tar.gz: 6154b76796fef327e488b722d1075b05f448be9fd499e4b8525e58b3544399d0
5
5
  SHA512:
6
- metadata.gz: b6675d6ee443f716836b6ce3680597d2ee9abcb5ff5999ba8cd452d03128d3794a5f0bc663d311d99401f4549887f77347be86d0fc84430c5b5b3e869b0bcf10
7
- data.tar.gz: 1f972ef62c9bf15ba0b0881206cb09bcb5986a8cd2f28875d703e3d372d61ce89e19cf12f049b6609416ead6d52704185f5791554e5967631d90327ef610e52a
6
+ metadata.gz: 16cf1d53448e132be9dd3c45dc755f933f434114dc13d7414b3670becaf97c4696ba0c57e3f10075f984b60516d1286d3ccee01bad8baa4bc2bad1143ea1d1f4
7
+ data.tar.gz: 69a336f08b80474be83a0d0111e74a7cfbed451e5dc3ca2dba41318e3212c0e10565f799be327fdc9a26eed111b7ef1a7a46d90f24e85151a4f0d1e719b909d9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2025-05-2
11
+
12
+ ### Added
13
+
14
+ - **Deep sideloading**: introduced Include scoping class to support deep sideloading using single query (e.g. include='author.posts')
15
+ - **Deep sorting**: added SortNormalizer to support sorting by deep sideloaded resource's attribute (e.g. sort='author.posts.title')
16
+ - **Links control**: Control the links(pagination and resource) in response via request query params (pagination_links=true|false, links=true|false)
17
+
10
18
  ## [1.0.0] - 2025-03-18
11
19
 
12
20
  ### Added
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  An adapter to make Graphiti work with the ActiveGraph(former Neo4jrb) OGM. This gem allows you to easily build [jsonapi.org](https://jsonapi.org) compatible APIs for GraphDB using [Graphiti](https://www.graphiti.dev) and [ActiveGraph](https://github.com/neo4jrb/activegraph).
6
6
 
7
- ### Installation
7
+ ## Installation
8
8
  Add this line to your application's `Gemfile`:
9
9
  ```ruby
10
10
  gem 'graphiti-activegraph'
@@ -20,7 +20,6 @@ Or install it yourself as:
20
20
  gem install graphiti-activegraph
21
21
  ```
22
22
 
23
-
24
23
  ## Usage
25
24
  While defining a Resource class, inherit it from `Graphiti::ActiveGraph::Resource`
26
25
  ```ruby
@@ -34,28 +33,42 @@ class RelationshipBackedResource < Graphiti::ActiveGraph::Resource
34
33
  end
35
34
  ```
36
35
 
37
- ### Documentation
38
- ##### **Key Differences from Graphiti**
39
- - **Efficient Sideloading:**
36
+ ## Documentation
37
+ ### Key Differences from Graphiti
38
+ #### Efficient Sideloading:
40
39
  Unlike Graphiti, which executes multiple queries for sideloading, graphiti-activegraph leverages `with_ordered_associations` from ActiveGraph to fetch sideloaded data in a single query, improving performance.
41
40
 
42
- - **Sideposting Behavior:**
41
+ #### Sideposting Behavior:
43
42
  graphiti-activegraph allows assigning and unassigning relationships via sideposting but does not support modifying a resource’s attributes through sideposting.
44
43
 
45
- - **Thread Context Handling:**
44
+ #### Thread Context Handling:
46
45
  Graphiti stores context using `Thread.current[]`, which does not persist across different fibers within the same thread. In graphiti-activegraph, when running on MRI (non-JRuby environments), the gem uses `thread_variable_get` and `thread_variable_set`. Ensuring the context remains consistent across different fibers in the same thread.
47
46
 
48
- ##### **New Features in graphiti-activegraph**
49
- ###### Rendering Preloaded Objects Without Extra Queries
47
+ ### New Features in graphiti-activegraph
48
+ #### Rendering Preloaded Objects Without Extra Queries
50
49
  graphiti-activegraph introduces two new methods on the Graphiti resource class:
51
50
  `with_preloaded_obj(record, params)` – Renders a single preloaded ActiveGraph object without querying the database.
52
51
  `all_with_preloaded(records, params)` – Renders multiple preloaded ActiveGraph objects without additional queries.
53
52
  **Note:** These methods assume that the provided records are final and will not apply Graphiti’s filtering, sorting, or scoping logic.
54
53
 
55
- ### Contributing
54
+ #### Efficient Deep Sorting
55
+ Graphiti does not natively support deep sorting (i.e., sorting based on attributes of associated resources), `graphiti-activegraph` adds this feature by allowing you to chain associations using dot (.) notation.
56
+ For example, to sort Post records based on their Author's Country's name, you can use following query string:
57
+ ```
58
+ sort=author.country.name&include=author.country
59
+ ```
60
+ Here, `Author` and `Country` are associated resources, and `name` is an attribute on `Country`. The posts will be sorted by the country's name. You can chain any number of associations in this way to achieve deep sorting.
61
+ Note: The `include` parameter must be present and include the full association path (`author.country`) for the sorting to work correctly.
62
+
63
+ #### Response Payload Links control
64
+ Control the links in response payload via request query params:
65
+ - `pagination_links=true|false` — toggle top-level pagination links
66
+ - `links=true|false` — toggle links inside each resource object
67
+
68
+ ## Contributing
56
69
  Bug reports and pull requests are welcome on GitHub at https://github.com/mrhardikjoshi/graphiti-activegraph. This project is intended to be a safe, welcoming space for collaboration.
57
70
 
58
- ### Release
71
+ ## Release
59
72
  1. Make sure version file is updated/incremented in master branch
60
73
  2. git checkout master
61
74
  3. git pull origin master
@@ -64,5 +77,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/mrhard
64
77
  6. gem build graphiti-activegraph.gemspec
65
78
  7. gem push graphiti-activegraph-1.2.3.gem
66
79
 
67
- ### License
80
+ ## License
68
81
  The gem is available as open-source under the terms of the MIT License.
@@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_dependency 'graphiti', '>= 1.6.4'
21
21
  spec.add_dependency 'activegraph', '>= 12.0.0.beta.5'
22
+ spec.add_dependency 'parslet', '>= 2.0.0'
23
+ spec.add_dependency 'activegraph-extensions', '>= 0.1.0'
22
24
 
23
25
  spec.add_development_dependency 'graphiti_spec_helpers', '>= 1.0.0'
24
26
  spec.add_development_dependency 'standard'
@@ -0,0 +1,66 @@
1
+ module Graphiti::ActiveGraph::JsonapiExt
2
+ class IncludeDirective < JSONAPI::IncludeDirective
3
+ attr_accessor :length, :retain_rel_limit
4
+
5
+ def initialize(include_args, options = {})
6
+ include_hash = JSONAPI::IncludeDirective::Parser.parse_include_args(include_args)
7
+ @retain_rel_limit = options.delete(:retain_rel_limit)
8
+ @hash = formulate_hash(include_hash, options)
9
+ @options = options
10
+ end
11
+
12
+ def keys
13
+ super.select(&method(:key?))
14
+ end
15
+
16
+ alias get []
17
+
18
+ def key?(key)
19
+ super && get(key).valid_length?
20
+ end
21
+
22
+ def [](key)
23
+ super&.descend(key) || {}
24
+ end
25
+
26
+ def descend(key)
27
+ length && length != '' ? dup.tap { |dup| dup.add_self_reference(key, length.to_i - 1) } : self
28
+ end
29
+
30
+ def valid_length?
31
+ length.nil? || length == '' || length.to_i.positive?
32
+ end
33
+
34
+ def to_hash
35
+ @hash.each_with_object({}) do |(key, value), hash|
36
+ key = "#{key}*#{value.length}".to_sym if value.length
37
+ hash[key] = value.to_hash unless value == self
38
+ end
39
+ end
40
+
41
+ def add_self_reference(key, length)
42
+ @hash = @hash.merge(key => self)
43
+ self.length = length
44
+ end
45
+
46
+ private
47
+
48
+ def formulate_hash(include_hash, options)
49
+ include_hash.each_with_object({}) do |(key, value), hash|
50
+ rel_name, rel_length = extract_rel_meta(key)
51
+
52
+ hash[rel_name] = self.class.new(value, options_with_retain_rel_limit(options)).tap do |directive|
53
+ directive.add_self_reference(rel_name, rel_length) if key.to_s.match?(/.+\*(\d*)\z/)
54
+ end
55
+ end
56
+ end
57
+
58
+ def extract_rel_meta(key)
59
+ Graphiti::ActiveGraph::Util::Transformers::RelationParam.new(key).split_rel_length(retain_rel_limit)
60
+ end
61
+
62
+ def options_with_retain_rel_limit(options)
63
+ options.merge(retain_rel_limit:)
64
+ end
65
+ end
66
+ end
@@ -36,12 +36,39 @@ module Graphiti::ActiveGraph
36
36
  []
37
37
  end
38
38
 
39
+ def include_directive
40
+ @include_directive ||= Graphiti::ActiveGraph::JsonapiExt::IncludeDirective.new(@include_param, retain_rel_limit: true)
41
+ end
42
+
39
43
  def parse_sort_criteria_hash(hash)
40
44
  hash.map { |key, value| [key.to_s.split('.').map(&:to_sym), value] }.to_h
41
45
  end
42
46
 
47
+ def links?
48
+ [:json, :xml, 'json', 'xml'].exclude?(params[:format]) && show_resource_links?
49
+ end
50
+
51
+ def pagination_links?
52
+ action != :find && show_pagination_links?
53
+ end
54
+
43
55
  private
44
56
 
57
+ def show_pagination_links?
58
+ return @show_pagination_links unless @show_pagination_links.nil?
59
+ @show_pagination_links = read_link_params(:pagination_links)
60
+ end
61
+
62
+ def show_resource_links?
63
+ return @show_resource_links unless @show_resource_links.nil?
64
+
65
+ @show_resource_links = read_link_params(:links)
66
+ end
67
+
68
+ def read_link_params(name)
69
+ ActiveModel::Type::Boolean.new.cast(params[name]) != false
70
+ end
71
+
45
72
  def sort_criteria(sort)
46
73
  sort.split(',').map(&method(:sort_hash)).map(&method(:parse_sort_criteria_hash))
47
74
  end
@@ -14,6 +14,7 @@ module Graphiti
14
14
  end
15
15
 
16
16
  def append_scopings(opts)
17
+ add_scoping(:include, Scoping::Include, opts)
17
18
  add_scoping(:association_eagerload, Scoping::AssociationEagerLoad, opts)
18
19
  end
19
20
 
@@ -1,6 +1,5 @@
1
1
  module Graphiti::ActiveGraph
2
2
  module Scoping
3
- # Handles sideloading via scoping instead of sideloading query as in original jsonapi_suite
4
3
  class AssociationEagerLoad < Graphiti::Scoping::Base
5
4
  def custom_scope
6
5
  nil
@@ -0,0 +1,43 @@
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?
14
+ self.scope = resource.handle_includes(scope, normalized_includes, normalized_sorts,
15
+ with_vars: with_vars_for_sort, paginate: paginate?)
16
+ end
17
+
18
+ private
19
+
20
+ attr_accessor :scope
21
+
22
+ def query
23
+ @opts[:query_obj]
24
+ end
25
+
26
+ def paginate?
27
+ Graphiti::Scoping::Paginate.new(@resource, @query_hash, scope, @opts).apply?
28
+ end
29
+
30
+ def normalized_sorts
31
+ Internal::SortNormalizer.new(scope).normalize(normalized_includes, query.sorts, query.deep_sort)
32
+ end
33
+
34
+ def include_normalizer
35
+ Internal::IncludeNormalizer
36
+ end
37
+
38
+ def normalized_includes
39
+ @normalized_includes ||= include_normalizer.new(resource.class, scope, query_hash[:fields]).normalize(query.include_hash)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +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
@@ -0,0 +1,94 @@
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Internal
4
+ # Determine valid paths specified in filter and sort API params and normalize them for neo4j
5
+ class PathDescriptor
6
+ attr_reader :scope, :path, :attribute, :rel
7
+ delegate :relationship_class, to: :rel, allow_nil: true
8
+
9
+ def initialize(scope, rel)
10
+ self.scope = scope
11
+ self.path = []
12
+ self.attribute = :id
13
+ self.rel = rel
14
+ @has_next = true
15
+ end
16
+
17
+ def next?
18
+ @has_next
19
+ end
20
+
21
+ def increment(key)
22
+ raise Exception, 'no continuation on path possible' unless next?
23
+ if rel
24
+ increment_from_cache(key)
25
+ else
26
+ increment_from_scope(key)
27
+ end
28
+ end
29
+
30
+ def declared_property
31
+ (relationship_class || scope).attributes[attribute]
32
+ end
33
+
34
+ def self.parse(scope, path, rel = nil)
35
+ path_desc = new(scope, rel)
36
+ path_desc.increment(path.shift) while path_desc.next? && path.present?
37
+ path_desc if path.empty?
38
+ end
39
+
40
+ def self.association_for_relationship(associations, key)
41
+ key_class_name = key[:rel_name].classify
42
+ key_assoc_name = key[:rel_name].gsub('_rel', '')
43
+ assocs = associations.find_all { |_, value| value.relationship_class_name == key_class_name || value.name.to_s == key_assoc_name }
44
+ assocs.size == 1 ? assocs.first : nil
45
+ end
46
+
47
+ def path_relationships
48
+ path.map { |elm| elm[:rel_name].to_sym }
49
+ end
50
+
51
+ private
52
+
53
+ attr_writer :scope, :path, :attribute, :rel
54
+
55
+ def increment_from_cache(key)
56
+ rel_name = key[:rel_name]
57
+ if rel.target_class_names.map(&:demodulize).map(&:downcase).include?(rel_name)
58
+ self.rel = nil
59
+ else
60
+ final_attribute(rel_name)
61
+ end
62
+ end
63
+
64
+ def increment_from_scope(key)
65
+ associations = scope.associations
66
+ if associations.key?(key[:rel_name].to_sym)
67
+ advance(key)
68
+ else
69
+ increment_from_rel(self.class.association_for_relationship(associations, key), key)
70
+ end
71
+ end
72
+
73
+ def increment_from_rel(entry, key)
74
+ if entry
75
+ advance(rel_name: entry.first.to_s)
76
+ self.rel = entry.last
77
+ else
78
+ final_attribute(key[:rel_name])
79
+ end
80
+ end
81
+
82
+ def advance(key)
83
+ path << key
84
+ self.scope = scope.send(key[:rel_name], rel_length: key[:rel_length])
85
+ end
86
+
87
+ def final_attribute(key)
88
+ self.attribute = key
89
+ @has_next = false
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,54 @@
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Internal
4
+ class SortNormalizer
5
+ attr_reader :scope
6
+
7
+ def initialize(scope)
8
+ @scope = scope
9
+ end
10
+
11
+ def normalize(includes_hash, sorts, deep_sorts)
12
+ normalized_deep_sort = normalize_deep_sort(includes_hash, deep_sorts || [])
13
+ normalized_base_sort = normalize_base_sort(sorts)
14
+ normalized_deep_sort.merge(normalized_base_sort)
15
+ end
16
+
17
+ def normalize_base_sort(sorts)
18
+ sorts.present? ? { '' => sorts.map { |sort| "#{sort.keys.first} #{sort.values.first}" } } : {}
19
+ end
20
+
21
+ def normalize_deep_sort(includes_hash, sorts)
22
+ sorts
23
+ .map { |sort| sort(includes_hash, sort) }
24
+ .compact
25
+ .group_by(&:first)
26
+ .map { |key, value| combined_order_spec(key, value) }
27
+ .to_h
28
+ end
29
+
30
+ private
31
+
32
+ def combined_order_spec(key, value)
33
+ [key.join('.'), value.map(&:last)]
34
+ end
35
+
36
+ def sort(includes_hash, sort)
37
+ path = sort.keys.first.map { |key| { rel_name: key.to_s } }
38
+ return nil unless (descriptor = PathDescriptor.parse(scope, path))
39
+
40
+ sort_spec(descriptor, sort.values.first) if valid_sort?(includes_hash.deep_dup, descriptor.path_relationships)
41
+ end
42
+
43
+ def valid_sort?(hash, rels)
44
+ rels.empty? || rels.all? { |rel| hash = hash[rel] || hash[:"#{rel.to_s + '*'}"] }
45
+ end
46
+
47
+ def sort_spec(descriptor, direction)
48
+ sort_attr = [descriptor.attribute, direction].join(' ')
49
+ [descriptor.path_relationships, descriptor.rel.present? ? { rel: sort_attr } : sort_attr]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Internal
4
+ # Carrying forward valriables from neo4j procedure call to sort with include
5
+ module SortingAliases
6
+ def with_vars_for_sort
7
+ [] unless add_extra_vars_to_query?
8
+ (deep_sort_keys + sort_keys) & resource.extra_attributes.keys
9
+ end
10
+
11
+ def add_extra_vars_to_query?
12
+ resource.extra_attributes.present? && (query.sorts.present? || query.deep_sort.present?)
13
+ end
14
+
15
+ def deep_sort_keys
16
+ (query.deep_sort || []).collect { |sort| sort.keys.first.first }
17
+ end
18
+
19
+ def sort_keys
20
+ query.sorts.collect(&:keys).flatten
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module Graphiti::ActiveGraph
2
+ module Scoping
3
+ module Internal
4
+ module SparseFieldsEagerloading
5
+ private
6
+
7
+ def add_relationships_from_sparse_fields(scope, includes_array)
8
+ return if @fields.blank?
9
+
10
+ related_fields(scope.model).each { |field_name| includes_array << process_field(field_name, scope) }
11
+ end
12
+
13
+ def process_field(field_name, _scope)
14
+ field_name
15
+ end
16
+
17
+ def resource_name_of(model)
18
+ model.model_name.plural.to_sym
19
+ end
20
+
21
+ def related_fields(model)
22
+ attr_and_rel_fields = @fields[resource_name_of(model)] || []
23
+ attr_and_rel_fields.select { |field_name| model.associations[field_name] }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module Graphiti::ActiveGraph
2
+ module Util
3
+ module Parsers
4
+ class RelChain < Parslet::Parser
5
+ VAR_CHAR = 'a-z_'
6
+
7
+ rule(:asterisk) { str('*') }
8
+ rule(:range) { str('..') }
9
+ rule(:dot) { str('.') }
10
+ rule(:none) { str('') }
11
+ rule(:number) { match('[\d]').repeat(1) }
12
+ rule(:number?) { number | none }
13
+ rule(:identifier) { match("[#{VAR_CHAR}]") >> match("[#{VAR_CHAR}0-9]").repeat(0) }
14
+ rule(:identifier?) { identifier | none }
15
+ rule(:rel_name) { identifier?.as(:rel_name) }
16
+ rule(:length) { asterisk.as(:ast) >> number?.maybe.as(:min) >> range.as(:range).maybe >> number?.maybe.as(:max) }
17
+ rule(:rel) { rel_name >> length.maybe }
18
+
19
+ rule(:rel_chain) { rel >> (dot >> rel).repeat(0) }
20
+ root(:rel_chain)
21
+
22
+ rule(:limit) { number?.as(:limit_digit) >> asterisk.as(:limit_ast) }
23
+ rule(:rel_param_rule) { limit.maybe.as(:limit_part) >> rel_name >> length.maybe.as(:length_part) }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ module Graphiti::ActiveGraph
2
+ module Util
3
+ module Transformers
4
+ class RelationParam
5
+ attr_reader :map
6
+
7
+ def initialize(relation_param_str)
8
+ @map = Graphiti::ActiveGraph::Util::Parsers::RelChain.new.rel_param_rule.parse(relation_param_str.to_s)
9
+ end
10
+
11
+ def split_rel_length(retain_rel_limit)
12
+ rel_name_part = if retain_rel_limit
13
+ (rel_limit || '') + rel_name
14
+ else
15
+ rel_name_sym
16
+ end
17
+ [rel_name_part.to_sym, rel_length_number]
18
+ end
19
+
20
+ def rel_name_n_length
21
+ "#{rel_name}#{rel_length}"
22
+ end
23
+
24
+ def rel_limit(limit_part = nil)
25
+ join(limit_part || map[:limit_part])
26
+ end
27
+
28
+ def rel_limit_number
29
+ rel_limit(map[:limit_part]&.except(:limit_ast))
30
+ end
31
+
32
+ def rel_name
33
+ map[:rel_name].to_s
34
+ end
35
+
36
+ def rel_name_sym
37
+ rel_name.to_sym
38
+ end
39
+
40
+ def rel_length(length_part = nil)
41
+ join(length_part || map[:length_part])
42
+ end
43
+
44
+ def rel_length_number
45
+ rel_length(map[:length_part]&.except(:ast))
46
+ end
47
+
48
+ private
49
+
50
+ def join(hash)
51
+ hash&.values&.map(&:to_s)&.join
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,5 +1,5 @@
1
1
  module Graphiti
2
2
  module ActiveGraph
3
- VERSION = '1.0.0'
3
+ VERSION = '1.1.0'
4
4
  end
5
5
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Graphiti
4
2
  module SidepostConfiguration
5
3
  extend ActiveSupport::Concern
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti-activegraph
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hardik Joshi
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-21 00:00:00.000000000 Z
10
+ date: 2025-05-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: graphiti
@@ -37,6 +37,34 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: 12.0.0.beta.5
40
+ - !ruby/object:Gem::Dependency
41
+ name: parslet
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.0.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.0.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: activegraph-extensions
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.1.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.1.0
40
68
  - !ruby/object:Gem::Dependency
41
69
  name: graphiti_spec_helpers
42
70
  requirement: !ruby/object:Gem::Requirement
@@ -166,6 +194,7 @@ files:
166
194
  - lib/graphiti/active_graph/extensions/resources/payload_combinable.rb
167
195
  - lib/graphiti/active_graph/extensions/resources/preloadable.rb
168
196
  - lib/graphiti/active_graph/extensions/resources/rel.rb
197
+ - lib/graphiti/active_graph/jsonapi_ext/include_directive.rb
169
198
  - lib/graphiti/active_graph/jsonapi_ext/serializable/resource_ext.rb
170
199
  - lib/graphiti/active_graph/query.rb
171
200
  - lib/graphiti/active_graph/request_validators/validator.rb
@@ -178,10 +207,18 @@ files:
178
207
  - lib/graphiti/active_graph/scoping/association_eager_load.rb
179
208
  - lib/graphiti/active_graph/scoping/filter.rb
180
209
  - lib/graphiti/active_graph/scoping/filterable.rb
210
+ - lib/graphiti/active_graph/scoping/include.rb
211
+ - lib/graphiti/active_graph/scoping/internal/include_normalizer.rb
212
+ - lib/graphiti/active_graph/scoping/internal/path_descriptor.rb
213
+ - lib/graphiti/active_graph/scoping/internal/sort_normalizer.rb
214
+ - lib/graphiti/active_graph/scoping/internal/sorting_aliases.rb
215
+ - lib/graphiti/active_graph/scoping/internal/sparse_fields_eagerloading.rb
181
216
  - lib/graphiti/active_graph/sideload_resolve.rb
217
+ - lib/graphiti/active_graph/util/parsers/rel_chain.rb
182
218
  - lib/graphiti/active_graph/util/relationship_payload.rb
183
219
  - lib/graphiti/active_graph/util/serializer_attribute.rb
184
220
  - lib/graphiti/active_graph/util/serializer_relationship.rb
221
+ - lib/graphiti/active_graph/util/transformers/relation_param.rb
185
222
  - lib/graphiti/active_graph/version.rb
186
223
  - lib/graphiti/sidepost_configuration.rb
187
224
  homepage: https://github.com/mrhardikjoshi/graphiti-activegraph