graphiti_gql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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