graphiti_gql 0.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +98 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +3 -0
  9. data/Rakefile +6 -0
  10. data/bin/bundle +114 -0
  11. data/bin/byebug +27 -0
  12. data/bin/coderay +27 -0
  13. data/bin/console +14 -0
  14. data/bin/graphiti +27 -0
  15. data/bin/htmldiff +27 -0
  16. data/bin/ldiff +27 -0
  17. data/bin/pry +27 -0
  18. data/bin/rake +27 -0
  19. data/bin/rspec +27 -0
  20. data/bin/setup +8 -0
  21. data/config/routes.rb +6 -0
  22. data/graphiti_gql.gemspec +46 -0
  23. data/lib/graphiti_gql/engine.rb +29 -0
  24. data/lib/graphiti_gql/errors.rb +21 -0
  25. data/lib/graphiti_gql/graphiti_hax.rb +71 -0
  26. data/lib/graphiti_gql/loaders/belongs_to.rb +63 -0
  27. data/lib/graphiti_gql/loaders/has_many.rb +14 -0
  28. data/lib/graphiti_gql/loaders/many.rb +79 -0
  29. data/lib/graphiti_gql/loaders/many_to_many.rb +16 -0
  30. data/lib/graphiti_gql/loaders/polymorphic_has_many.rb +17 -0
  31. data/lib/graphiti_gql/response_shim.rb +13 -0
  32. data/lib/graphiti_gql/schema/connection.rb +57 -0
  33. data/lib/graphiti_gql/schema/fields/attribute.rb +46 -0
  34. data/lib/graphiti_gql/schema/fields/index.rb +33 -0
  35. data/lib/graphiti_gql/schema/fields/show.rb +33 -0
  36. data/lib/graphiti_gql/schema/fields/stats.rb +54 -0
  37. data/lib/graphiti_gql/schema/fields/to_many.rb +37 -0
  38. data/lib/graphiti_gql/schema/fields/to_one.rb +47 -0
  39. data/lib/graphiti_gql/schema/list_arguments.rb +127 -0
  40. data/lib/graphiti_gql/schema/polymorphic_belongs_to_interface.rb +35 -0
  41. data/lib/graphiti_gql/schema/query.rb +62 -0
  42. data/lib/graphiti_gql/schema/registry.rb +67 -0
  43. data/lib/graphiti_gql/schema/resource_type.rb +100 -0
  44. data/lib/graphiti_gql/schema/util.rb +74 -0
  45. data/lib/graphiti_gql/schema.rb +46 -0
  46. data/lib/graphiti_gql/version.rb +3 -0
  47. data/lib/graphiti_gql.rb +62 -0
  48. metadata +188 -0
@@ -0,0 +1,71 @@
1
+ # These should be in Graphiti itself, but can't do it quite yet b/c GQL coupling.
2
+ # Ideally we eventually rip out the parts of Graphiti we need and roll this into
3
+ # that effort.
4
+ module ResourceExtras
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class << self
9
+ attr_accessor :graphql_name
10
+ end
11
+ end
12
+
13
+ class_methods do
14
+ def attribute(*args)
15
+ super(*args).tap do
16
+ opts = args.extract_options!
17
+ att = config[:attributes][args[0]]
18
+ att[:deprecation_reason] = opts[:deprecation_reason]
19
+ att[:null] = opts.key?(:null) ? opts[:null] : args[0] != :id
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ Graphiti::Resource.send(:include, ResourceExtras)
26
+
27
+ module FilterExtras
28
+ def filter_param
29
+ default_filter = resource.default_filter if resource.respond_to?(:default_filter)
30
+ default_filter ||= {}
31
+ default_filter.merge(super)
32
+ end
33
+ end
34
+ Graphiti::Scoping::Filter.send(:prepend, FilterExtras)
35
+
36
+ # ==================================================
37
+ # Below is all to support pagination argument 'last'
38
+ # ==================================================
39
+ module SortExtras
40
+ def sort_param
41
+ param = super
42
+ if query_hash[:reverse]
43
+ param = [{ id: :asc }] if param == []
44
+ param = param.map do |p|
45
+ {}.tap do |hash|
46
+ dir = p[p.keys.first]
47
+ dir = dir == :asc ? :desc : :asc
48
+ hash[p.keys.first] = dir
49
+ end
50
+ end
51
+ end
52
+ param
53
+ end
54
+ end
55
+ Graphiti::Scoping::Sort.send(:prepend, SortExtras)
56
+ module QueryExtras
57
+ def hash
58
+ hash = super
59
+ hash[:reverse] = true if @params[:reverse]
60
+ hash
61
+ end
62
+ end
63
+ Graphiti::Query.send(:prepend, QueryExtras)
64
+ module ScopeExtras
65
+ def resolve(*args)
66
+ results = super
67
+ results.reverse! if @query.hash[:reverse]
68
+ results
69
+ end
70
+ end
71
+ Graphiti::Scope.send(:prepend, ScopeExtras)
@@ -0,0 +1,63 @@
1
+ module GraphitiGql
2
+ module Loaders
3
+ class FakeRecord < Struct.new(:id, :type)
4
+ end
5
+
6
+ class BelongsTo < GraphQL::Batch::Loader
7
+ def initialize(sideload, params)
8
+ @sideload = sideload
9
+ @params = params
10
+ end
11
+
12
+ def perform(ids)
13
+ # process nils
14
+ ids.each { |id| fulfill(id, nil) if id.nil? }
15
+ ids.compact!
16
+ return if ids.empty?
17
+
18
+ if @params[:simpleid]
19
+ if @sideload.type == :polymorphic_belongs_to
20
+ ids.each do |id|
21
+ child = @sideload.children.values.find { |c| c.group_name == id[:type].to_sym }
22
+ type = Schema::Registry.instance.get(child.resource.class, interface: false)[:type]
23
+ fulfill(id, FakeRecord.new(id[:id], type))
24
+ end
25
+ else
26
+ type = Schema::Registry.instance.get(@sideload.resource.class)[:type]
27
+ ids.each { |id| fulfill(id, FakeRecord.new(id, type)) }
28
+ end
29
+ return
30
+ end
31
+
32
+ if @sideload.type == :polymorphic_belongs_to
33
+ groups = ids.group_by { |hash| hash[:type] }
34
+ payload = {}
35
+ groups.each_pair do |key, val|
36
+ payload[key] = val.map { |v| v[:id] }
37
+ end
38
+ futures = []
39
+ payload.each_pair do |key, value|
40
+ params = { filter: {} }
41
+ klass = @sideload.children.values.find { |c| c.group_name == key.to_sym }
42
+ params = {
43
+ filter: { id: { eq: value.join(",") } }
44
+ }
45
+ futures << Concurrent::Future.execute do
46
+ { type: key, data: klass.resource.class.all(params).data }
47
+ end
48
+ end
49
+ values = futures.map(&:value)
50
+ ids.each do |id|
51
+ val = values.find { |v| v[:type] == id[:type] }
52
+ fulfill(id, val[:data][0])
53
+ end
54
+ else
55
+ params = {filter: {id: {eq: ids.join(",")}}}
56
+ records = @sideload.resource.class.all(params).data
57
+ map = records.index_by { |record| record.id }
58
+ ids.each { |id| fulfill(id, map[id]) }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ module GraphitiGql
2
+ module Loaders
3
+ class HasMany < Many
4
+ def assign(ids, proxy)
5
+ records = proxy.data
6
+ map = records.group_by { |record| record.send(@sideload.foreign_key) }
7
+ ids.each do |id|
8
+ data = [map[id] || [], proxy]
9
+ fulfill(id, data)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ module GraphitiGql
2
+ module Loaders
3
+ class Many < GraphQL::Batch::Loader
4
+ def self.factory(sideload, params)
5
+ if sideload.polymorphic_as
6
+ PolymorphicHasMany.for(sideload, params)
7
+ elsif sideload.type == :many_to_many
8
+ ManyToMany.for(sideload, params)
9
+ else
10
+ HasMany.for(sideload, params)
11
+ end
12
+ end
13
+
14
+ def initialize(sideload, params)
15
+ @sideload = sideload
16
+ @params = params
17
+ end
18
+
19
+ def perform(ids)
20
+ raise ::Graphiti::Errors::UnsupportedPagination if paginating? && ids.length > 1
21
+ raise Errors::UnsupportedStats if requesting_stats? && ids.length > 1 && !can_group?
22
+
23
+ build_params(ids)
24
+ proxy = @sideload.resource.class.all(@params)
25
+ assign(ids, proxy)
26
+ end
27
+
28
+ def assign(ids, proxy)
29
+ raise "implement in subclass"
30
+ end
31
+
32
+ private
33
+
34
+ def build_params(ids)
35
+ @params[:filter] ||= {}
36
+
37
+ if @sideload.polymorphic_as
38
+ type = ids[0][:"#{@sideload.polymorphic_as}_type"]
39
+ foreign_keys = ids.map { |id| id[@sideload.foreign_key] }
40
+ @params[:filter][:"#{@sideload.polymorphic_as}_type"] = type
41
+ @params[:filter][@sideload.foreign_key] = foreign_keys.join(",")
42
+ elsif @sideload.type == :many_to_many
43
+ fk = @sideload.foreign_key.values.first
44
+ @params[:filter].merge!(fk => { eq: ids.join(",") })
45
+ else
46
+ @params[:filter].merge!(@sideload.foreign_key => { eq: ids.join(",") })
47
+ end
48
+
49
+ if @params[:stats]
50
+ group_by = if @sideload.type ==:many_to_many
51
+ @sideload.foreign_key.values.first
52
+ else
53
+ @sideload.foreign_key
54
+ end
55
+ @params[:stats][:group_by] = group_by
56
+ end
57
+
58
+ unless @params.key?(:page) && @params[:page].key?(:size)
59
+ @params[:page] ||= {}
60
+ @params[:page][:size] = 999
61
+ end
62
+ end
63
+
64
+ def paginating?
65
+ pagination_key_present = @params.key?(:page) &&
66
+ [:size, :last, :before, :after].any? { |arg| @params[:page].key?(arg) }
67
+ pagination_key_present && @params[:page][:size] != 0 # stats
68
+ end
69
+
70
+ def requesting_stats?
71
+ @params.key?(:stats)
72
+ end
73
+
74
+ def can_group?
75
+ @sideload.resource.adapter.can_group?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ module GraphitiGql
2
+ module Loaders
3
+ class ManyToMany < Many
4
+ def assign(ids, proxy)
5
+ records = proxy.data
6
+ thru = @sideload.foreign_key.keys.first
7
+ fk = @sideload.foreign_key[thru]
8
+ ids.each do |id|
9
+ match = ->(thru) { thru.send(fk) == id }
10
+ corresponding = records.select { |record| record.send(thru).any?(&match) }
11
+ fulfill(id, [corresponding, proxy])
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module GraphitiGql
2
+ module Loaders
3
+ class PolymorphicHasMany < Many
4
+ def assign(ids, proxy)
5
+ records = proxy.data
6
+ ids.each do |id|
7
+ corresponding = records.select do |record|
8
+ record.send("#{@sideload.polymorphic_as}_type") == id[:"#{@sideload.polymorphic_as}_type"] &&
9
+ record.send(@sideload.foreign_key) == id[@sideload.foreign_key]
10
+ end
11
+ data = [corresponding || [], proxy]
12
+ fulfill(id, data)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # We need the raw records, but also the proxy so we can grab stats
2
+ module GraphitiGql
3
+ class Schema
4
+ class ResponseShim
5
+ attr_reader :data, :proxy
6
+
7
+ def initialize(data, proxy)
8
+ @data = data
9
+ @proxy = proxy
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class Connection < ::GraphQL::Pagination::Connection
4
+ def nodes
5
+ return @items if @run_once
6
+ @proxy = @items.proxy
7
+ @items = @items.data
8
+ @run_once = true
9
+ @items
10
+ end
11
+
12
+ def proxy
13
+ nodes
14
+ @proxy
15
+ end
16
+
17
+ def has_previous_page
18
+ proxy.pagination.has_previous_page?
19
+ end
20
+
21
+ def has_next_page
22
+ nodes
23
+ return false if @items.length.zero?
24
+ cursor = JSON.parse(Base64.decode64(cursor_for(@items.last)))
25
+ cursor["offset"] < @proxy.pagination.send(:item_count)
26
+ end
27
+
28
+ def cursor_for(item)
29
+ nodes
30
+ starting_offset = 0
31
+ page_param = proxy.query.pagination
32
+ if (page_number = page_param[:number])
33
+ page_size = page_param[:size] || proxy.resource.default_page_size
34
+ starting_offset = (page_number - 1) * page_size
35
+ end
36
+
37
+ if (cursor = page_param[:after])
38
+ starting_offset = cursor[:offset]
39
+ end
40
+
41
+ current_offset = @items.index(item)
42
+ offset = starting_offset + current_offset + 1 # (+ 1 b/c o-base index)
43
+ Base64.encode64({offset: offset}.to_json).chomp
44
+ end
45
+ end
46
+
47
+ class ToManyConnection < Connection
48
+ def nodes
49
+ return @items if @run_once
50
+ @proxy = @items[1]
51
+ @items = @items[0]
52
+ @run_once = true
53
+ @items
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ module Fields
4
+ class Attribute
5
+ def initialize(name, config)
6
+ @name = name
7
+ @config = config
8
+ end
9
+
10
+ def apply(type)
11
+ is_nullable = !!@config[:null]
12
+ _config = @config
13
+ _name = @name
14
+ opts = @config.slice(:null, :deprecation_reason)
15
+ type.field(@name, field_type, **opts)
16
+ type.define_method @name do
17
+ if (readable = _config[:readable]).is_a?(Symbol)
18
+ resource = object.instance_variable_get(:@__graphiti_resource)
19
+ unless resource.send(readable)
20
+ path = Graphiti.context[:object][:current_path].join(".")
21
+ raise Errors::UnauthorizedField.new(path)
22
+ end
23
+ end
24
+ value = if _config[:proc]
25
+ instance_eval(&_config[:proc])
26
+ else
27
+ object.send(_name)
28
+ end
29
+ return if value.nil?
30
+ Graphiti::Types[_config[:type]][:read].call(value)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def field_type
37
+ canonical_graphiti_type = Graphiti::Types.name_for(@config[:type])
38
+ field_type = GQL_TYPE_MAP[canonical_graphiti_type.to_sym]
39
+ field_type = String if @name == :id
40
+ field_type = [field_type] if @config[:type].to_s.starts_with?("array_of")
41
+ field_type
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ module Fields
4
+ class Index
5
+ def initialize(registered)
6
+ @registered = registered
7
+ end
8
+
9
+ def apply(query)
10
+ resource = @registered[:resource]
11
+ field = query.field resource.graphql_entrypoint,
12
+ @registered[:type].connection_type,
13
+ null: false,
14
+ connection: false,
15
+ extensions: [RelayConnectionExtension],
16
+ extras: [:lookahead]
17
+ ListArguments.new(resource).apply(field)
18
+ query.define_method name do |**arguments|
19
+ params = Util.params_from_args(arguments)
20
+ proxy = resource.all(params)
21
+ ResponseShim.new(proxy.data, proxy)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def name
28
+ @registered[:resource].graphql_entrypoint
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ module Fields
4
+ class Show
5
+ def initialize(registered)
6
+ @registered = registered
7
+ end
8
+
9
+ def apply(query)
10
+ field = query.field name,
11
+ @registered[:type],
12
+ null: true,
13
+ extras: [:lookahead]
14
+ field.argument(:id, String, required: true)
15
+ _registered = @registered
16
+ query.define_method name do |**arguments|
17
+ params = Util.params_from_args(arguments)
18
+ _registered[:resource].all(params).data[0]
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def name
25
+ @registered[:resource]
26
+ .graphql_entrypoint.to_s
27
+ .underscore
28
+ .singularize.to_sym
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,54 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ module Fields
4
+ class Stats
5
+ def initialize(resource)
6
+ @resource = resource
7
+ end
8
+
9
+ def apply(type)
10
+ type.field :stats, build_stat_class, null: false
11
+ type.define_method :stats do
12
+ # Process grouped (to-many relationship) stats
13
+ stats = object.proxy.stats.deep_dup
14
+ stats.each_pair do |attr, calc|
15
+ calc.each_pair do |calc_name, value|
16
+ if value.is_a?(Hash)
17
+ stats[attr][calc_name] = value[parent.id]
18
+ end
19
+ end
20
+ end
21
+ stats
22
+ end
23
+ type
24
+ end
25
+
26
+ private
27
+
28
+ def build_stat_class
29
+ name = Registry.instance.key_for(@resource)
30
+ stat_graphql_name = "#{name}Stats"
31
+ return Registry.instance[stat_graphql_name][:type] if Registry.instance[stat_graphql_name]
32
+ klass = Class.new(GraphQL::Schema::Object)
33
+ klass.graphql_name(stat_graphql_name)
34
+ @resource.stats.each_pair do |name, config|
35
+ calc_class = build_calc_class(stat_graphql_name, name, config.calculations.keys)
36
+ klass.field name, calc_class, null: false
37
+ end
38
+ Registry.instance[stat_graphql_name] = { type: klass }
39
+ klass
40
+ end
41
+
42
+ def build_calc_class(stat_graphql_name, stat_name, calculations)
43
+ name = "#{stat_graphql_name}#{stat_name}Calculations"
44
+ klass = Class.new(GraphQL::Schema::Object)
45
+ klass.graphql_name(name)
46
+ calculations.each do |calc|
47
+ klass.field calc, Float, null: false
48
+ end
49
+ klass
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ module Fields
4
+ class ToMany
5
+ def initialize(sideload, sideload_type)
6
+ @sideload = sideload
7
+ @sideload_type = sideload_type
8
+ end
9
+
10
+ def apply(type)
11
+ field = type.field @sideload.name,
12
+ @sideload_type.connection_type,
13
+ null: false,
14
+ connection: false,
15
+ extensions: [RelayConnectionExtension],
16
+ extras: [:lookahead]
17
+ ListArguments.new(@sideload.resource.class, @sideload).apply(field)
18
+ _sideload = @sideload
19
+ type.define_method(@sideload.name) do |**arguments|
20
+ Util.is_readable_sideload!(_sideload)
21
+ params = Util.params_from_args(arguments)
22
+ pk = object.send(_sideload.primary_key)
23
+ id = if _sideload.polymorphic_as
24
+ hash = {}
25
+ hash[_sideload.foreign_key] = pk
26
+ hash[:"#{_sideload.polymorphic_as}_type"] = object.class.name
27
+ id = hash
28
+ else
29
+ id = pk
30
+ end
31
+ Loaders::Many.factory(_sideload, params).load(id)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ module Fields
4
+ class ToOne
5
+ def initialize(sideload, sideload_type)
6
+ @sideload = sideload
7
+ @sideload_type = sideload_type
8
+ end
9
+
10
+ def apply(type)
11
+ field = type.field @sideload.name,
12
+ @sideload_type,
13
+ null: true,
14
+ extras: [:lookahead]
15
+ _sideload = @sideload
16
+ type.define_method(@sideload.name) do |**arguments|
17
+ Util.is_readable_sideload!(_sideload)
18
+
19
+ if _sideload.type == :has_one
20
+ id = object.send(_sideload.primary_key)
21
+ params = { filter: { _sideload.foreign_key => { eq: id } } }
22
+ return _sideload.resource.class.all(params).data[0]
23
+ end
24
+
25
+ lookahead = arguments[:lookahead]
26
+ id = object.send(_sideload.foreign_key)
27
+ if id.nil?
28
+ Loaders::BelongsTo.for(_sideload, {}).load(nil)
29
+ else
30
+ params = Util.params_from_args(arguments)
31
+
32
+ if _sideload.type == :polymorphic_belongs_to
33
+ id = { id: id, type: object.send(_sideload.grouper.field_name) }
34
+ end
35
+
36
+ selections = lookahead.selections.map(&:name).sort
37
+ if selections == [:id] || selections == [:__typename, :id]
38
+ params[:simpleid] = true
39
+ end
40
+ Loaders::BelongsTo.for(_sideload, params).load(id)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end