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,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