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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +25 -12
- data/graphiti-activegraph.gemspec +2 -0
- data/lib/graphiti/active_graph/jsonapi_ext/include_directive.rb +66 -0
- data/lib/graphiti/active_graph/query.rb +27 -0
- data/lib/graphiti/active_graph/scope.rb +1 -0
- data/lib/graphiti/active_graph/scoping/association_eager_load.rb +0 -1
- data/lib/graphiti/active_graph/scoping/include.rb +43 -0
- data/lib/graphiti/active_graph/scoping/internal/include_normalizer.rb +82 -0
- data/lib/graphiti/active_graph/scoping/internal/path_descriptor.rb +94 -0
- data/lib/graphiti/active_graph/scoping/internal/sort_normalizer.rb +54 -0
- data/lib/graphiti/active_graph/scoping/internal/sorting_aliases.rb +25 -0
- data/lib/graphiti/active_graph/scoping/internal/sparse_fields_eagerloading.rb +28 -0
- data/lib/graphiti/active_graph/util/parsers/rel_chain.rb +27 -0
- data/lib/graphiti/active_graph/util/transformers/relation_param.rb +56 -0
- data/lib/graphiti/active_graph/version.rb +1 -1
- data/lib/graphiti/sidepost_configuration.rb +0 -2
- metadata +39 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c387f138fe40ab7a304c5d587a0281258e2bf4e8304d7e0f0967295adeeef6d7
|
4
|
+
data.tar.gz: 6154b76796fef327e488b722d1075b05f448be9fd499e4b8525e58b3544399d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
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.
|
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-
|
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
|