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,127 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class ListArguments
4
+ class SortDirType < GraphQL::Schema::Enum
5
+ graphql_name "SortDir"
6
+ value "asc", "Ascending"
7
+ value "desc", "Descending"
8
+ end
9
+
10
+ def initialize(resource, sideload = nil)
11
+ @resource = resource
12
+ @sideload = sideload
13
+ end
14
+
15
+ def apply(field)
16
+ define_filters(field) unless @resource.filters.empty?
17
+ define_sorts(field) unless @resource.sorts.empty?
18
+ end
19
+
20
+ private
21
+
22
+ def registry
23
+ Registry.instance
24
+ end
25
+
26
+ def define_filters(field)
27
+ filter_type = generate_filter_type(field)
28
+ required = @resource.filters.any? { |name, config|
29
+ value = !!config[:required]
30
+ if @sideload
31
+ value && @sideload.foreign_key != name
32
+ else
33
+ value
34
+ end
35
+ }
36
+ field.argument :filter, filter_type, required: required
37
+ end
38
+
39
+ def generate_filter_type(field)
40
+ type_name = "#{registry.key_for(@resource)}Filter"
41
+ if (registered = registry[type_name])
42
+ return registered[:type]
43
+ end
44
+ klass = Class.new(GraphQL::Schema::InputObject)
45
+ klass.graphql_name type_name
46
+ @resource.filters.each_pair do |name, config|
47
+ attr_type = generate_filter_attribute_type(type_name, name, config)
48
+ klass.argument name.to_s.camelize(:lower),
49
+ attr_type,
50
+ required: !!config[:required]
51
+ end
52
+ registry[type_name] = { type: klass }
53
+ klass
54
+ end
55
+
56
+ def generate_filter_attribute_type(type_name, filter_name, filter_config)
57
+ klass = Class.new(GraphQL::Schema::InputObject)
58
+ filter_graphql_name = "#{type_name}Filter#{filter_name.to_s.camelize(:lower)}"
59
+ klass.graphql_name(filter_graphql_name)
60
+ filter_config[:operators].keys.each do |operator|
61
+ canonical_graphiti_type = Graphiti::Types
62
+ .name_for(filter_config[:type])
63
+ type = GQL_TYPE_MAP[canonical_graphiti_type]
64
+ type = String if filter_name == :id
65
+ required = !!filter_config[:required] && operator == "eq"
66
+
67
+ if (allowlist = filter_config[:allow])
68
+ type = define_allowlist_type(filter_graphql_name, allowlist)
69
+ end
70
+
71
+ type = [type] unless !!filter_config[:single]
72
+ klass.argument operator, type, required: required
73
+ end
74
+ klass
75
+ end
76
+
77
+ def define_allowlist_type(filter_graphql_name, allowlist)
78
+ name = "#{filter_graphql_name}Allow"
79
+ if (registered = registry[name])
80
+ return registered[:type]
81
+ end
82
+ klass = Class.new(GraphQL::Schema::Enum)
83
+ klass.graphql_name(name)
84
+ allowlist.each do |allowed|
85
+ klass.value(allowed)
86
+ end
87
+ registry[name] = { type: klass }
88
+ klass
89
+ end
90
+
91
+ def define_sorts(field)
92
+ sort_type = generate_sort_type
93
+ field.argument :sort, [sort_type], required: false
94
+ end
95
+
96
+ def generate_sort_att_type
97
+ type_name = "#{registry.key_for(@resource)}SortAtt"
98
+ if (registered = registry[type_name])
99
+ return registered[:type]
100
+ end
101
+ klass = Class.new(GraphQL::Schema::Enum) {
102
+ graphql_name(type_name)
103
+ }
104
+ @resource.sorts.each_pair do |name, config|
105
+ klass.value name.to_s.camelize(:lower), "Sort by #{name}"
106
+ end
107
+ registry[type_name] = { type: klass }
108
+ klass
109
+ end
110
+
111
+ def generate_sort_type
112
+ type_name = "#{registry.key_for(@resource)}Sort"
113
+ if (registered = registry[type_name])
114
+ return registered[:type]
115
+ end
116
+ att_type = generate_sort_att_type
117
+ klass = Class.new(GraphQL::Schema::InputObject) {
118
+ graphql_name type_name
119
+ argument :att, att_type, required: true
120
+ argument :dir, SortDirType, required: true
121
+ }
122
+ registry[type_name] = { type: klass }
123
+ klass
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,35 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class PolymorphicBelongsToInterface
4
+ def initialize(resource, sideload)
5
+ @resource = resource
6
+ @sideload = sideload
7
+ end
8
+
9
+ def build
10
+ return registry[name][:type] if registry[name]
11
+
12
+ klass = Module.new
13
+ klass.send :include, ResourceType::BaseInterface
14
+ klass.field :id, String, null: false
15
+ klass.field :_type, String, null: false
16
+ klass.graphql_name(name)
17
+ @sideload.children.values.each do |child|
18
+ registry.get(child.resource.class)[:type].implements(klass)
19
+ end
20
+ registry[name] = { type: klass }
21
+ registry[name]
22
+ end
23
+
24
+ private
25
+
26
+ def registry
27
+ Registry.instance
28
+ end
29
+
30
+ def name
31
+ "#{registry.key_for(@resource)}__#{@sideload.name}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class Query
4
+ def initialize(resources, existing_query: nil)
5
+ @resources = resources
6
+ @query_class = Class.new(existing_query || ::GraphQL::Schema::Object)
7
+ @query_class.graphql_name "Query"
8
+ @query_class.field_class ::GraphQL::Schema::Field
9
+ end
10
+
11
+ def build
12
+ @resources.each { |resource| ResourceType.new(resource).build }
13
+ define_entrypoints
14
+ add_relationships
15
+ @query_class
16
+ end
17
+
18
+ private
19
+
20
+ def registry
21
+ Registry.instance
22
+ end
23
+
24
+ def define_entrypoints
25
+ registry.resource_types.each do |registered|
26
+ if GraphitiGql.entrypoint?(registered[:resource])
27
+ Fields::Index.new(registered).apply(@query_class)
28
+ Fields::Show.new(registered).apply(@query_class)
29
+ end
30
+ end
31
+ end
32
+
33
+ def add_relationships
34
+ each_relationship do |type, sideload_type, sideload|
35
+ if [:has_many, :many_to_many].include?(sideload.type)
36
+ Fields::ToMany.new(sideload, sideload_type).apply(type)
37
+ else
38
+ Fields::ToOne.new(sideload, sideload_type).apply(type)
39
+ end
40
+ end
41
+ end
42
+
43
+ def each_relationship
44
+ registry.resource_types.each do |registered|
45
+ registered[:resource].sideloads.each do |name, sl|
46
+ next unless sl.readable?
47
+
48
+ registered_sl = if sl.type == :polymorphic_belongs_to
49
+ PolymorphicBelongsToInterface
50
+ .new(registered[:resource], sl)
51
+ .build
52
+ else
53
+ registry.get(sl.resource.class)
54
+ end
55
+
56
+ yield registered[:type], registered_sl[:type], sl
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,67 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class Registry
4
+ include Singleton
5
+
6
+ def initialize
7
+ clear
8
+ end
9
+
10
+ def get(object, interface: true)
11
+ @data[key_for(object, interface: interface)]
12
+ end
13
+
14
+ def set(resource, type, interface: true)
15
+ @data[key_for(resource, interface: interface)] = { resource: resource, type: type, interface: interface }
16
+ end
17
+
18
+ def key_for(object, interface: true)
19
+ if object.ancestors.include?(Graphiti::Resource)
20
+ key = key_for_resource(object)
21
+ if object.polymorphic?
22
+ if !object.polymorphic_child? && interface
23
+ key = "I#{key}"
24
+ end
25
+ end
26
+ key
27
+ else
28
+ raise 'unknown object!'
29
+ end
30
+ end
31
+
32
+ def clear
33
+ @data = {}
34
+ end
35
+
36
+ def []=(key, value)
37
+ @data[key] = value
38
+ end
39
+
40
+ def [](key)
41
+ @data[key]
42
+ end
43
+
44
+ def key?(key)
45
+ @data.key?(key)
46
+ end
47
+
48
+ def values
49
+ @data.values
50
+ end
51
+
52
+ # When polymorphic parent, returns the Interface not the Class
53
+ def resource_types
54
+ values
55
+ .select { |v| v.key?(:resource) && !v[:interface] }
56
+ .map { |registered| get(registered[:resource]) }
57
+ end
58
+
59
+ private
60
+
61
+ def key_for_resource(resource)
62
+ resource.graphql_name ||
63
+ resource.name.gsub('Resource', '').gsub('::', '')
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,100 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class ResourceType
4
+ module BaseInterface
5
+ include GraphQL::Schema::Interface
6
+
7
+ definition_methods do
8
+ # Optional: if this method is defined, it overrides `Schema.resolve_type`
9
+ def resolve_type(object, context)
10
+ return object.type if object.is_a?(Loaders::FakeRecord)
11
+ resource = object.instance_variable_get(:@__graphiti_resource)
12
+ Registry.instance.get(resource.class)[:type]
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize(resource, implements: nil)
18
+ @resource = resource
19
+ @implements = implements
20
+ end
21
+
22
+ def build
23
+ return registry.get(@resource)[:type] if registry.get(@resource)
24
+ type = build_base_type
25
+ registry_name = registry.key_for(@resource, interface: poly_parent?)
26
+ type.connection_type_class(build_connection_class)
27
+ type.graphql_name(registry_name)
28
+ type.implements(@implements) if @implements
29
+ add_fields(type, @resource)
30
+ registry.set(@resource, type, interface: poly_parent?)
31
+ process_polymorphic_parent(type) if poly_parent?
32
+ type
33
+ end
34
+
35
+ private
36
+
37
+ def process_polymorphic_parent(type)
38
+ # Define the actual class that implements the interface
39
+ registry.set(@resource, type, interface: false)
40
+ @resource.children.each do |child|
41
+ if (registered = registry.get(child))
42
+ registered[:type].implements(type)
43
+ else
44
+ self.class.new(child, implements: type).build
45
+ end
46
+ end
47
+ end
48
+
49
+ def poly_parent?
50
+ @resource.polymorphic? && !@resource.polymorphic_child?
51
+ end
52
+
53
+ def build_base_type
54
+ klass = nil
55
+ if poly_parent?
56
+ type_name = "I#{name}"
57
+ klass = Module.new
58
+ klass.send(:include, BaseInterface)
59
+ ctx = nil
60
+ klass.definition_methods { ctx = self }
61
+ ctx.define_method :resolve_type do |object, context|
62
+ resource = object.instance_variable_get(:@__graphiti_resource)
63
+ registry_name = Registry.instance.key_for(resource.class)
64
+ if resource.polymorphic?
65
+ resource = resource.class.resource_for_model(object)
66
+ registry_name = Registry.instance.key_for(resource)
67
+ end
68
+ Registry.instance[registry_name][:type]
69
+ end
70
+ else
71
+ klass = Class.new(GraphQL::Schema::Object)
72
+ end
73
+
74
+ klass
75
+ end
76
+
77
+ def registry
78
+ Registry.instance
79
+ end
80
+
81
+ def name
82
+ registry.key_for(@resource)
83
+ end
84
+
85
+ def add_fields(type, resource)
86
+ resource.attributes.each_pair do |name, config|
87
+ if config[:readable]
88
+ Fields::Attribute.new(name, config).apply(type)
89
+ end
90
+ end
91
+ end
92
+
93
+ def build_connection_class
94
+ klass = Class.new(GraphQL::Types::Relay::BaseConnection)
95
+ Fields::Stats.new(@resource).apply(klass)
96
+ klass
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,74 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ class Util
4
+ def self.params_from_args(arguments)
5
+ lookahead = arguments.delete(:lookahead)
6
+ params = arguments.as_json.deep_transform_keys { |key| key.to_s.underscore.to_sym }
7
+ if params[:sort]
8
+ params[:sort] = Util.transform_sort_param(params[:sort])
9
+ end
10
+
11
+ if (first = params.delete(:first))
12
+ params[:page] ||= {}
13
+ params[:page][:size] = first
14
+ end
15
+
16
+
17
+ if (last = params.delete(:last))
18
+ params[:page] ||= {}
19
+ params[:page][:size] = last
20
+ params[:reverse] = true
21
+ end
22
+
23
+ if (after = params.delete(:after))
24
+ params[:page] ||= {}
25
+ params[:page][:after] = after
26
+ end
27
+
28
+ if (before = params.delete(:before))
29
+ params[:page] ||= {}
30
+ params[:page][:before] = before
31
+ end
32
+
33
+ if (id = params.delete(:id))
34
+ params[:filter] ||= {}
35
+ params[:filter][:id] = { eq: id }
36
+ end
37
+
38
+ if lookahead.selects?(:stats)
39
+ stats = lookahead.selection(:stats)
40
+ payload = {}
41
+ stats.selections.map(&:name).each do |name|
42
+ payload[name] = stats.selection(name).selections.map(&:name)
43
+ end
44
+ params[:stats] = payload
45
+
46
+ # only requesting stats
47
+ if lookahead.selections.map(&:name) == [:stats]
48
+ params[:page] = { size: 0 }
49
+ end
50
+ end
51
+
52
+ params
53
+ end
54
+
55
+ def self.transform_sort_param(sorts)
56
+ sorts.map do |sort_param|
57
+ sort = sort_param[:att].underscore
58
+ sort = "-#{sort}" if sort_param[:dir] == "desc"
59
+ sort
60
+ end.join(",")
61
+ end
62
+
63
+ def self.is_readable_sideload!(sideload)
64
+ readable = sideload.instance_variable_get(:@readable)
65
+ if readable.is_a?(Symbol)
66
+ path = Graphiti.context[:object][:current_path].join(".")
67
+ unless sideload.parent_resource.send(readable)
68
+ raise Errors::UnauthorizedField.new(path)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,46 @@
1
+ module GraphitiGql
2
+ class Schema
3
+ GQL_TYPE_MAP = {
4
+ integer_id: String,
5
+ string: String,
6
+ uuid: String,
7
+ integer: Integer,
8
+ float: Float,
9
+ boolean: GraphQL::Schema::Member::GraphQLTypeNames::Boolean,
10
+ date: GraphQL::Types::ISO8601Date,
11
+ datetime: GraphQL::Types::ISO8601DateTime,
12
+ hash: GraphQL::Types::JSON,
13
+ array: [GraphQL::Types::JSON],
14
+ array_of_strings: [String],
15
+ array_of_integers: [Integer],
16
+ array_of_floats: [Float],
17
+ array_of_dates: [GraphQL::Types::ISO8601Date],
18
+ array_of_datetimes: [GraphQL::Types::ISO8601DateTime]
19
+ }
20
+
21
+ class RelayConnectionExtension < GraphQL::Schema::Field::ConnectionExtension
22
+ def resolve(object:, arguments:, context:)
23
+ next_args = arguments.dup
24
+ yield(object, next_args, arguments)
25
+ end
26
+ end
27
+
28
+ def initialize(resources)
29
+ @resources = resources
30
+ end
31
+
32
+ def generate
33
+ klass = Class.new(::GraphQL::Schema)
34
+ klass.query(Query.new(@resources).build)
35
+ klass.use(GraphQL::Batch)
36
+ klass.connections.add(ResponseShim, Connection)
37
+ klass.connections.add(Array, ToManyConnection)
38
+ klass
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+
45
+
46
+
@@ -0,0 +1,3 @@
1
+ module GraphitiGql
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,62 @@
1
+ require "active_support/core_ext/object/json"
2
+ require "graphql"
3
+ require 'graphql/batch'
4
+ require "graphiti_gql/graphiti_hax"
5
+ require "graphiti_gql/version"
6
+ require "graphiti_gql/errors"
7
+ require "graphiti_gql/loaders/many"
8
+ require "graphiti_gql/loaders/has_many"
9
+ require "graphiti_gql/loaders/many_to_many"
10
+ require "graphiti_gql/loaders/polymorphic_has_many"
11
+ require "graphiti_gql/loaders/belongs_to"
12
+ require "graphiti_gql/response_shim"
13
+ require "graphiti_gql/schema"
14
+ require "graphiti_gql/schema/connection"
15
+ require "graphiti_gql/schema/registry"
16
+ require "graphiti_gql/schema/util"
17
+ require "graphiti_gql/schema/query"
18
+ require "graphiti_gql/schema/resource_type"
19
+ require "graphiti_gql/schema/polymorphic_belongs_to_interface"
20
+ require "graphiti_gql/schema/list_arguments"
21
+ require "graphiti_gql/schema/fields/show"
22
+ require "graphiti_gql/schema/fields/index"
23
+ require "graphiti_gql/schema/fields/to_many"
24
+ require "graphiti_gql/schema/fields/to_one"
25
+ require "graphiti_gql/schema/fields/attribute"
26
+ require "graphiti_gql/schema/fields/stats"
27
+ require "graphiti_gql/engine" if defined?(Rails)
28
+
29
+ module GraphitiGql
30
+ class Error < StandardError; end
31
+
32
+ def self.schema!
33
+ Schema::Registry.instance.clear
34
+ resources ||= Graphiti.resources.reject(&:abstract_class?)
35
+ @schema = Schema.new(resources).generate
36
+ end
37
+
38
+ def self.schema
39
+ @schema
40
+ end
41
+
42
+ def self.entrypoints=(val)
43
+ @entrypoints = val
44
+ end
45
+
46
+ def self.entrypoints
47
+ @entrypoints
48
+ end
49
+
50
+ def self.entrypoint?(resource)
51
+ @entrypoints.nil? || @entrypoints.include?(resource)
52
+ end
53
+
54
+ def self.run(query_string, variables = {}, context = {})
55
+ Graphiti.with_context(context) do
56
+ result = schema.execute query_string,
57
+ variables: variables,
58
+ context: context
59
+ result.to_h
60
+ end
61
+ end
62
+ end