graphiti_graphql 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.
@@ -0,0 +1,313 @@
1
+ module GraphitiGraphQL
2
+ class Runner
3
+ def execute(query_string, variables, schema)
4
+ query = GraphQL::Query.new(schema, query_string, variables: variables)
5
+ definition = query.document.definitions.first
6
+ selection = definition.selections.first
7
+ resource_class = find_entrypoint_resource_class(selection.name)
8
+
9
+ # TODO: instead, keep track of fields we add
10
+ Util.with_gql_context do
11
+ if resource_class
12
+ run_query(schema, resource_class, selection, query)
13
+ else
14
+ Graphiti.graphql_schema.schema.execute query_string,
15
+ variables: variables,
16
+ context: GraphitiGraphQL.get_context
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def run_query(schema, resource_class, selection, query)
24
+ if (errors = collect_errors(schema, query)).any?
25
+ {"errors" => errors.map(&:to_h)}
26
+ else
27
+ params = process_selection(selection, {}, query.variables.to_h)
28
+ json = resource_class.all(params).as_graphql
29
+ render(json, selection.name)
30
+ end
31
+ end
32
+
33
+ def render(json, selection_name)
34
+ payload = if find_one?(selection_name)
35
+ {selection_name.to_sym => json.values[0][0]}
36
+ else
37
+ json
38
+ end
39
+ {data: payload}
40
+ end
41
+
42
+ def find_one?(selection_name)
43
+ selection_name == selection_name.singularize
44
+ end
45
+
46
+ def collect_errors(schema, query)
47
+ query.analysis_errors = schema.analysis_engine
48
+ .analyze_query(query, query.analyzers || [])
49
+ query.validation_errors + query.analysis_errors + query.context.errors
50
+ end
51
+
52
+ # We can't just constantize the name from the schema
53
+ # Because classes can be reopened and modified in tests (or elsewhere, in theory)
54
+ def find_entrypoint_resource_class(entrypoint)
55
+ Graphiti.resources.find(&matches_entrypoint?(entrypoint))
56
+ end
57
+
58
+ def find_entrypoint_schema_resource(entrypoint)
59
+ graphiti_schema.resources.find(&matches_entrypoint?(entrypoint))
60
+ end
61
+
62
+ def matches_entrypoint?(entrypoint)
63
+ lambda do |resource|
64
+ resource.graphql_entrypoint.to_s.underscore == entrypoint.pluralize.underscore
65
+ end
66
+ end
67
+
68
+ def introspection_query?(query)
69
+ query.document.definitions.first.selections.first.name == "__schema"
70
+ end
71
+
72
+ def find_resource_by_selection_name(name)
73
+ graphiti_schema.resources
74
+ .find { |r| r.type == name.pluralize.underscore }
75
+ end
76
+
77
+ def graphiti_schema
78
+ Graphiti.graphql_schema.graphiti_schema
79
+ end
80
+
81
+ def schema_resource_for_selection(selection, parent_resource)
82
+ if parent_resource
83
+ parent_resource.related_resource(selection.name.underscore.to_sym)
84
+ else
85
+ find_entrypoint_schema_resource(selection.name)
86
+ end
87
+ end
88
+
89
+ def process_selection(
90
+ selection,
91
+ params,
92
+ variables_hash,
93
+ parent_resource = nil,
94
+ parent_name_chain = nil,
95
+ fragment_jsonapi_type: nil
96
+ )
97
+ selection_name = selection.name.underscore
98
+
99
+ pbt = false # polymorphic_belongs_to
100
+ if parent_resource
101
+ pbt = parent_resource.pbt?(selection_name.to_sym)
102
+ end
103
+
104
+ chained_name = nil
105
+ if fragment_jsonapi_type
106
+ selection_name = "on__#{fragment_jsonapi_type}--#{selection_name}"
107
+ end
108
+
109
+ if parent_resource
110
+ chained_name = selection_name
111
+ if parent_name_chain
112
+ chained_name = [parent_name_chain, selection_name].join(".")
113
+ end
114
+ end
115
+
116
+ if !pbt
117
+ resource = schema_resource_for_selection(selection, parent_resource)
118
+ gather_filters(params, selection, variables_hash, chained_name)
119
+ gather_sorts(params, selection, variables_hash, chained_name)
120
+ gather_pages(params, selection, variables_hash, chained_name)
121
+ end
122
+
123
+ params[:include] ||= []
124
+ params[:include] << chained_name if chained_name
125
+
126
+ fragments = selection.selections.select { |s|
127
+ s.is_a?(GraphQL::Language::Nodes::InlineFragment)
128
+ }
129
+ non_fragments = selection.selections - fragments
130
+
131
+ if pbt
132
+ # Only id/_type possible here
133
+ fields, extra_fields, sideload_selections = [], [], []
134
+ fields = non_fragments.map { |s| s.name.underscore }
135
+ # If fragments specified, these will get merged in later
136
+ if fragments.empty?
137
+ params[:fields][chained_name] = fields.join(",")
138
+ end
139
+ else
140
+ fields, extra_fields, sideload_selections =
141
+ gather_fields(non_fragments, resource, params, chained_name)
142
+
143
+ sideload_selections.each do |sideload_selection|
144
+ process_selection(sideload_selection, params, variables_hash, resource, chained_name)
145
+ end
146
+ end
147
+
148
+ fragments.each do |fragment|
149
+ resource_name = Graphiti.graphql_schema.type_registry[fragment.type.name][:resource]
150
+ klass = graphiti_schema.resources.find { |r| r.name == resource_name }
151
+ _, _, fragment_sideload_selections = gather_fields fragment.selections,
152
+ klass,
153
+ params,
154
+ nil, # no chaining supported here
155
+ polymorphic_parent_data: [fields, extra_fields, sideload_selections]
156
+
157
+ fragment_sideload_selections.each do |sideload_selection|
158
+ fragment_jsonapi_type = klass.type
159
+ process_selection(sideload_selection, params, variables_hash, klass, chained_name, fragment_jsonapi_type: fragment_jsonapi_type)
160
+ end
161
+ end
162
+
163
+ params
164
+ end
165
+
166
+ def gather_fields(
167
+ selections,
168
+ resource,
169
+ params,
170
+ chained_name,
171
+ polymorphic_parent_data: nil
172
+ )
173
+ fields, extra_fields, sideload_selections = [], [], []
174
+ selections.each do |sel|
175
+ selection_name = sel.name.underscore
176
+ sideload = resource.sideloads[selection_name.to_sym]
177
+ if sideload && !sideload.remote?
178
+ sideload_selections << sel
179
+ else
180
+ field_name = sel.name.underscore
181
+ if resource.extra_attributes[field_name.to_sym]
182
+ extra_fields << field_name
183
+ else
184
+ fields << field_name
185
+ end
186
+ end
187
+ end
188
+
189
+ if polymorphic_parent_data
190
+ fields |= polymorphic_parent_data[0]
191
+ extra_fields |= polymorphic_parent_data[1]
192
+ sideload_selections |= polymorphic_parent_data[2]
193
+ end
194
+
195
+ params[:fields] ||= {}
196
+ params[:extra_fields] ||= {}
197
+ if chained_name
198
+ field_param_name = chained_name
199
+
200
+ # If this is a polymorphic fragment subselection, the field is just the
201
+ # jsonapi type, for simplicity. TODO: Won't work if double-listing
202
+ last_chain = chained_name.split(".").last
203
+ if last_chain.starts_with?("on__")
204
+ field_param_name = last_chain.split("--")[1]
205
+ end
206
+ # Remove the special on__ flag from the chain, since not used for fields
207
+ field_param_name = field_param_name.gsub(/on__.*--/, "")
208
+
209
+ params[:fields][field_param_name.to_sym] = fields.join(",")
210
+ if extra_fields.present?
211
+ params[:extra_fields][field_param_name.to_sym] = extra_fields.join(",")
212
+ end
213
+ else
214
+ params[:fields][resource.type.to_sym] = fields.join(",")
215
+ if extra_fields.present?
216
+ params[:extra_fields][resource.type.to_sym] = extra_fields.join(",")
217
+ end
218
+ end
219
+
220
+ [fields, extra_fields, sideload_selections]
221
+ end
222
+
223
+ def gather_filters(params, selection, variable_hash, chained_name = nil)
224
+ filters = {}.tap do |f|
225
+ arg = selection.arguments.find { |arg| arg.name == "filter" }
226
+ arg ||= selection.arguments.find { |arg| arg.name == "id" }
227
+
228
+ if arg
229
+ if arg.name == "filter"
230
+ arg.children[0].arguments.each do |attr_arg|
231
+ field_name = attr_arg.name.underscore
232
+ filter_param_name = [chained_name, field_name].compact.join(".")
233
+
234
+ attr_arg.value.arguments.each do |operator_arg|
235
+ value = operator_arg.value
236
+ if value.respond_to?(:name) # is a variable
237
+ value = variable_hash[operator_arg.value.name]
238
+ end
239
+ f[filter_param_name] = {operator_arg.name.underscore => value}
240
+ end
241
+ end
242
+ else
243
+ value = arg.value
244
+ if value.respond_to?(:name) # is a variable
245
+ value = variable_hash[arg.value.name]
246
+ end
247
+ f[:id] = {eq: value}
248
+ end
249
+ end
250
+ end
251
+
252
+ if filters
253
+ params[:filter] ||= {}
254
+ params[:filter].merge!(filters)
255
+ end
256
+ end
257
+
258
+ def gather_sorts(params, selection, variable_hash, chained_name = nil)
259
+ sorts = [].tap do |s|
260
+ selection.arguments.each do |arg|
261
+ if arg.name == "sort"
262
+ value = if arg.value.respond_to?(:name) # is a variable
263
+ variable_hash[arg.value.name].map(&:to_h)
264
+ else
265
+ arg.value.map(&:to_h)
266
+ end
267
+ jsonapi_values = value.map { |v|
268
+ att = (v[:att] || v["att"]).underscore
269
+ att = [chained_name, att].compact.join(".")
270
+ if v["dir"] == "desc"
271
+ att = "-#{att}"
272
+ end
273
+ att
274
+ }
275
+
276
+ s << jsonapi_values.join(",")
277
+ end
278
+ end
279
+ end
280
+
281
+ if sorts.present?
282
+ params[:sort] = [params[:sort], sorts].compact.join(",")
283
+ end
284
+ end
285
+
286
+ def gather_pages(params, selection, variable_hash, chained_name = nil)
287
+ pages = {}.tap do |p|
288
+ selection.arguments.each do |arg|
289
+ if arg.name == "page"
290
+ value = if arg.value.respond_to?(:name) # is a variable
291
+ variable_hash[arg.value.name].to_h
292
+ else
293
+ arg.value.to_h
294
+ end
295
+
296
+ if chained_name
297
+ value.each_pair do |k, v|
298
+ p["#{chained_name}.#{k}"] = v
299
+ end
300
+ else
301
+ p.merge!(value)
302
+ end
303
+ end
304
+ end
305
+ end
306
+
307
+ if pages.present?
308
+ params[:page] ||= {}
309
+ params[:page].merge!(pages)
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,538 @@
1
+ module GraphitiGraphQL
2
+ class Schema
3
+ GQL_TYPE_MAP = {
4
+ integer_id: String,
5
+ string: String,
6
+ integer: Integer,
7
+ float: Float,
8
+ boolean: GraphQL::Schema::Member::GraphQLTypeNames::Boolean,
9
+ date: GraphQL::Types::ISO8601Date,
10
+ datetime: GraphQL::Types::ISO8601DateTime,
11
+ hash: GraphQL::Types::JSON,
12
+ array: [GraphQL::Types::JSON],
13
+ array_of_strings: [String],
14
+ array_of_integers: [Integer],
15
+ array_of_floats: [Float],
16
+ array_of_dates: [GraphQL::Types::ISO8601Date],
17
+ array_of_datetimes: [GraphQL::Types::ISO8601DateTime]
18
+ }
19
+
20
+ class BaseField < GraphQL::Schema::Field
21
+ end
22
+
23
+ class BaseObject < GraphQL::Schema::Object
24
+ end
25
+
26
+ module BaseInterface
27
+ include GraphQL::Schema::Interface
28
+ end
29
+
30
+ class << self
31
+ attr_accessor :entrypoints, :federation
32
+ attr_writer :base_field, :base_object, :base_interface
33
+ end
34
+
35
+ attr_accessor :type_registry, :schema, :graphiti_schema
36
+
37
+ def self.federation?
38
+ !!@federation
39
+ end
40
+
41
+ def self.base_field
42
+ @base_field || BaseField
43
+ end
44
+
45
+ def self.base_object
46
+ @base_object || BaseObject
47
+ end
48
+
49
+ def self.base_interface
50
+ @base_interface || BaseInterface
51
+ end
52
+
53
+ def self.generate(entrypoint_resources = nil)
54
+ instance = new
55
+ schema = Class.new(::GraphitiGraphQL.schema_class || GraphQL::Schema)
56
+
57
+ if federation?
58
+ schema.send(:include, ApolloFederation::Schema)
59
+ end
60
+
61
+ graphiti_schema = GraphitiGraphQL::GraphitiSchema::Wrapper
62
+ .new(Graphiti::Schema.generate)
63
+ # TODO: if we avoid this on federation, or remove altogether
64
+ # Make sure we don't blow up
65
+ # graphiti_schema.merge_remotes!
66
+
67
+ entries = entrypoint_resources || entrypoints
68
+ instance.apply_query(graphiti_schema, schema, entries)
69
+
70
+ # NB if we add mutation support, make sure this is applied after
71
+ if federation?
72
+ schema.use GraphQL::Batch
73
+ end
74
+ instance.schema = schema
75
+ instance.graphiti_schema = graphiti_schema
76
+ instance
77
+ end
78
+
79
+ def self.resource_class_for_type(type_name)
80
+ const_get "#{type_name}Resource".gsub("__", "")
81
+ end
82
+
83
+ def initialize
84
+ @type_registry = {}
85
+ end
86
+
87
+ # TODO put this in a Federation::Schema module
88
+ # Maybe even the External classes themselves?
89
+ # TODO assign/assign_each
90
+ def apply_federation(graphiti_schema, graphql_schema)
91
+ type_registry.each_pair do |name, config|
92
+ if config[:resource]
93
+ local_type = config[:type]
94
+ local_resource = Graphiti.resources
95
+ .find { |r| r.name == config[:resource] }
96
+ # TODO: maybe turn off the graphiti debug for these?
97
+ local_type.define_singleton_method :resolve_reference do |reference, context, lookahead|
98
+ Federation::BelongsToLoader
99
+ .for(local_resource, lookahead.selections.map(&:name))
100
+ .load(reference[:id])
101
+ end
102
+ end
103
+ end
104
+
105
+ # NB: test already registered bc 2 things have same relationship
106
+ GraphitiGraphQL::Federation.external_resources.each_pair do |klass_name, config|
107
+ pre_registered = !!type_registry[klass_name]
108
+ external_klass = if pre_registered
109
+ type_registry[klass_name][:type]
110
+ else
111
+ external_klass = Class.new(self.class.base_object)
112
+ external_klass.graphql_name klass_name
113
+ external_klass
114
+ end
115
+
116
+ unless pre_registered
117
+ external_klass.key(fields: "id")
118
+ external_klass.extend_type
119
+ external_klass.field :id, String, null: false, external: true
120
+ external_klass.class_eval do
121
+ def self.resolve_reference(reference, _context, _lookup)
122
+ reference
123
+ end
124
+ end
125
+ end
126
+
127
+ unless pre_registered
128
+ # NB must be registered before processing rels
129
+ type_registry[klass_name] = { type: external_klass }
130
+ end
131
+
132
+ # TODO: only do it if field not already defined
133
+ config.relationships.each_pair do |name, relationship|
134
+ if relationship.has_many?
135
+ define_federated_has_many(graphiti_schema, external_klass, relationship)
136
+ elsif relationship.belongs_to?
137
+ define_federated_belongs_to(config, relationship)
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ # TODO: refactor to not constantly pass schemas around
144
+ def define_federated_has_many(graphiti_schema, external_klass, relationship)
145
+ local_name = GraphitiGraphQL::GraphitiSchema::Resource
146
+ .gql_name(relationship.local_resource_class.name)
147
+ local_type = type_registry[local_name][:type]
148
+ local_resource_name = type_registry[local_name][:resource]
149
+ local_resource = Graphiti.resources.find { |r| r.name == local_resource_name }
150
+
151
+ local_interface = type_registry["I#{local_name}"]
152
+ best_type = local_interface ? local_interface[:type] : local_type
153
+
154
+ field = external_klass.field relationship.name,
155
+ [best_type],
156
+ null: false,
157
+ extras: [:lookahead]
158
+
159
+ define_arguments_for_sideload_field(field, graphiti_schema.get_resource(local_resource_name))
160
+ external_klass.define_method relationship.name do |lookahead:, **arguments|
161
+ # TODO test params...do version of sort with array/symbol keys and plain string
162
+ params = arguments.as_json
163
+ .deep_transform_keys { |key| key.to_s.underscore.to_sym }
164
+ selections = lookahead.selections.map(&:name)
165
+ selections << relationship.foreign_key
166
+ selections << :_type # polymorphism
167
+ params[:fields] = { local_resource.type => selections.join(",") }
168
+
169
+ if (sort = Util.parse_sort(params[:sort]))
170
+ params[:sort] = sort
171
+ end
172
+
173
+ Federation::HasManyLoader
174
+ .for(local_resource, params, relationship.foreign_key)
175
+ .load(object[:id])
176
+ end
177
+ end
178
+
179
+ def define_federated_belongs_to(external_resource_config, relationship)
180
+ type_name = GraphitiSchema::Resource.gql_name(relationship.local_resource_class.name)
181
+ local_type = type_registry[type_name][:type]
182
+
183
+ # Todo maybe better way here
184
+ interface = type_registry["I#{type_name}"]
185
+
186
+ local_type = interface[:type] if interface
187
+ local_resource_name = type_registry[type_name][:resource]
188
+ local_resource_class = Graphiti.resources.find { |r| r.name == local_resource_name }
189
+
190
+ local_types = [local_type]
191
+ if interface
192
+ local_types |= interface[:implementers]
193
+ end
194
+
195
+ local_types.each do |local|
196
+ local.field relationship.name,
197
+ type_registry[external_resource_config.type_name][:type], # todo need to define the type?
198
+ null: true
199
+ end
200
+ end
201
+
202
+ def apply_query(graphiti_schema, graphql_schema, entries)
203
+ query_type = generate_schema_query(graphql_schema, graphiti_schema, entries)
204
+ if self.class.federation?
205
+ apply_federation(graphiti_schema, schema)
206
+ end
207
+
208
+ # NB - don't call .query here of federation will break things
209
+ if graphql_schema.instance_variable_get(:@query_object)
210
+ graphql_schema.instance_variable_set(:@query_object, nil)
211
+ graphql_schema.instance_variable_set(:@federation_query_object, nil)
212
+ end
213
+ graphql_schema.orphan_types(orphans(graphql_schema))
214
+ graphql_schema.query(query_type)
215
+ graphql_schema.query # Actually fires the federation code
216
+ end
217
+
218
+ def generate_schema_query(graphql_schema, graphiti_schema, entrypoint_resources = nil)
219
+ existing_query = graphql_schema.instance_variable_get(:@query) || graphql_schema.send(:find_inherited_value, :query)
220
+ # NB - don't call graphql_schema.query here of federation will break things
221
+ query_class = Class.new(existing_query || self.class.base_object)
222
+ # NB MUST be Query or federation-ruby will break things
223
+ query_class.graphql_name "Query"
224
+
225
+ entrypoints(graphiti_schema, entrypoint_resources).each do |resource|
226
+ next if resource.remote?
227
+ generate_type(resource)
228
+
229
+ add_index(query_class, resource)
230
+ add_show(query_class, resource)
231
+ end
232
+ query_class
233
+ end
234
+
235
+ def orphans(graphql_schema)
236
+ [].tap do |orphans|
237
+ type_registry.keys.each do |type_name|
238
+ unless graphql_schema.types.has_key?(type_name)
239
+ klass = type_registry[type_name][:type]
240
+ orphans << klass if klass.is_a?(Class)
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ private
247
+
248
+ def add_index(query_class, resource)
249
+ field = query_class.field resource.graphql_entrypoint,
250
+ [type_registry[resource.graphql_class_name][:type]],
251
+ "List #{resource.graphql_class_name(false).pluralize}",
252
+ null: false
253
+ define_arguments_for_sideload_field(field, resource)
254
+ end
255
+
256
+ def add_show(query_class, resource)
257
+ entrypoint = resource.graphql_entrypoint.to_s.singularize.to_sym
258
+ field = query_class.field entrypoint,
259
+ type_registry[resource.graphql_class_name][:type],
260
+ "Single #{resource.graphql_class_name(false).singularize}",
261
+ null: true
262
+ define_arguments_for_sideload_field field,
263
+ resource,
264
+ top_level_single: true
265
+ end
266
+
267
+ def entrypoints(graphiti_schema, manually_specified)
268
+ resources = graphiti_schema.resources
269
+ if manually_specified
270
+ resources = resources.select { |r|
271
+ manually_specified.map(&:name).include?(r.name)
272
+ }
273
+ end
274
+ resources
275
+ end
276
+
277
+ def generate_sort_att_type_for(resource)
278
+ type_name = "#{resource.graphql_class_name(false)}SortAtt"
279
+ if (registered = type_registry[type_name])
280
+ return registered[:type]
281
+ end
282
+ klass = Class.new(GraphQL::Schema::Enum) {
283
+ graphql_name(type_name)
284
+ }
285
+ resource.sorts.each_pair do |name, config|
286
+ klass.value name.to_s.camelize(:lower), "Sort by #{name}"
287
+ end
288
+ register(type_name, klass, resource)
289
+ klass
290
+ end
291
+
292
+ def generate_sort_type(resource)
293
+ type_name = "#{resource.graphql_class_name(false)}Sort"
294
+ if (registered = type_registry[type_name])
295
+ return registered[:type]
296
+ end
297
+ att_type = generate_sort_att_type_for(resource)
298
+ klass = Class.new(GraphQL::Schema::InputObject) {
299
+ graphql_name type_name
300
+ argument :att, att_type, required: true
301
+ argument :dir, SortDirType, required: true
302
+ }
303
+ register(type_name, klass)
304
+ klass
305
+ end
306
+
307
+ def define_arguments_for_sideload_field(field, resource, top_level_single: false)
308
+ if top_level_single
309
+ field.argument(:id, String, required: true)
310
+ else
311
+ sort_type = generate_sort_type(resource)
312
+ field.argument :sort, [sort_type], required: false
313
+ field.argument :page, PageType, required: false
314
+
315
+ filter_type = generate_filter_type(field, resource)
316
+ required = resource.filters.any? { |name, config| !!config[:required] }
317
+ field.argument :filter, filter_type, required: required
318
+ end
319
+ end
320
+
321
+ def generate_filter_type(field, resource)
322
+ type_name = "#{resource.graphql_class_name(false)}Filter"
323
+ if (registered = type_registry[type_name])
324
+ return registered[:type]
325
+ end
326
+ klass = Class.new(GraphQL::Schema::InputObject)
327
+ klass.graphql_name type_name
328
+ resource.filters.each_pair do |name, config|
329
+ attr_type = generate_filter_attribute_type(type_name, name, config)
330
+ klass.argument name.to_s.camelize(:lower),
331
+ attr_type,
332
+ required: !!config[:required]
333
+ end
334
+ register(type_name, klass)
335
+ klass
336
+ end
337
+
338
+ # TODO guarded operators or otherwise whatever eq => nil is
339
+ def generate_filter_attribute_type(type_name, filter_name, filter_config)
340
+ klass = Class.new(GraphQL::Schema::InputObject)
341
+ klass.graphql_name "#{type_name}Filter#{filter_name.to_s.camelize(:lower)}"
342
+ filter_config[:operators].each do |operator|
343
+ canonical_graphiti_type = Graphiti::Types
344
+ .name_for(filter_config[:type])
345
+ type = GQL_TYPE_MAP[canonical_graphiti_type]
346
+ required = !!filter_config[:required] && operator == "eq"
347
+ klass.argument operator, type, required: required
348
+ end
349
+ klass
350
+ end
351
+
352
+ def generate_resource_for_sideload(sideload)
353
+ if sideload.type == :polymorphic_belongs_to
354
+ unless registered?(sideload.parent_resource)
355
+ generate_type(sideload.parent_resource)
356
+ end
357
+ else
358
+ unless registered?(sideload.resource)
359
+ generate_type(sideload.resource)
360
+ end
361
+ end
362
+ end
363
+
364
+ def add_relationships_to_type_class(type_class, resource, processed = [])
365
+ type_name = resource.graphql_class_name(false)
366
+ return if processed.include?(type_name)
367
+
368
+ resource.sideloads.each_pair do |name, sideload|
369
+ next if sideload.remote?
370
+ generate_resource_for_sideload(sideload)
371
+
372
+ gql_type = if sideload.type == :polymorphic_belongs_to
373
+ interface_for_pbt(resource, sideload)
374
+ else
375
+ type_registry[sideload.graphql_class_name][:type]
376
+ end
377
+
378
+ gql_field_type = sideload.to_many? ? [gql_type] : gql_type
379
+ field_name = name.to_s.camelize(:lower)
380
+ unless type_class.fields[field_name]
381
+ field = type_class.field field_name.to_sym,
382
+ gql_field_type,
383
+ null: !sideload.to_many?
384
+
385
+ # No sort/filter/paginate on belongs_to
386
+ # unless sideload.type.to_s.include?('belongs_to')
387
+ unless sideload.type == :polymorphic_belongs_to
388
+ define_arguments_for_sideload_field(field, sideload.resource)
389
+ end
390
+ end
391
+
392
+ processed << type_name
393
+
394
+ # For PBT, the relationships are only possible on fragments
395
+ unless sideload.type == :polymorphic_belongs_to
396
+ add_relationships_to_type_class(gql_type, sideload.resource, processed)
397
+ end
398
+ end
399
+ end
400
+
401
+ def generate_type(resource, implements = nil)
402
+ return if resource.remote?
403
+ return if registered?(resource)
404
+ type_name = resource.graphql_class_name(false)
405
+
406
+ # Define the interface
407
+ klass = nil
408
+ poly_parent = resource.polymorphic? && !implements
409
+
410
+ if poly_parent
411
+ type_name = "I#{type_name}"
412
+ klass = Module.new
413
+ klass.send(:include, self.class.base_interface)
414
+ klass.definition_methods do
415
+ def resolve_type(object, context)
416
+ Graphiti.graphql_schema.schema.types[object[:__typename]]
417
+ end
418
+ end
419
+ else
420
+ klass = Class.new(self.class.base_object)
421
+ end
422
+ klass.graphql_name type_name
423
+
424
+ if implements
425
+ implement(klass, type_registry[implements])
426
+ end
427
+
428
+ if self.class.federation?
429
+ klass.key fields: "id"
430
+ end
431
+
432
+ klass.field(:_type, String, null: false)
433
+ resource.all_attributes.each do |name, config|
434
+ if config[:readable]
435
+ canonical_graphiti_type = Graphiti::Types.name_for(config[:type])
436
+ gql_type = GQL_TYPE_MAP[canonical_graphiti_type.to_sym]
437
+ gql_type = String if name == :id
438
+ # Todo document we don't have the concept, but can build it
439
+ is_nullable = !(name == :id)
440
+ klass.field(name, gql_type, null: is_nullable)
441
+ end
442
+ end
443
+
444
+ register(type_name, klass, resource, poly_parent)
445
+
446
+ resource.sideloads.each_pair do |name, sideload|
447
+ if sideload.type == :polymorphic_belongs_to
448
+ sideload.child_resources.each do |child_resource|
449
+ unless registered?(child_resource)
450
+ generate_type(child_resource)
451
+ end
452
+ end
453
+ else
454
+ unless registered?(sideload.resource)
455
+ generate_type(sideload.resource)
456
+ end
457
+ end
458
+ end
459
+
460
+ # Define the actual class that implements the interface
461
+ if poly_parent
462
+ canonical_name = resource.graphql_class_name(false)
463
+ klass = Class.new(self.class.base_object)
464
+ implement(klass, type_registry[type_name])
465
+ klass.graphql_name canonical_name
466
+ register(canonical_name, klass, resource)
467
+ end
468
+
469
+ if poly_parent
470
+ resource.children.each do |child|
471
+ if registered?(child)
472
+ child_klass = type_registry[child.graphql_class_name][:type]
473
+ child_klass.implements(type_registry[type_name][:type])
474
+ else
475
+ generate_type(child, type_name)
476
+ end
477
+ end
478
+ end
479
+
480
+ add_relationships_to_type_class(klass, resource)
481
+
482
+ klass
483
+ end
484
+
485
+ def registered?(resource)
486
+ name = resource.graphql_class_name(false)
487
+ !!type_registry[name]
488
+ end
489
+
490
+ def register(name, klass, resource = nil, interface = nil)
491
+ value = {type: klass}
492
+ value[:resource] = resource.name if resource
493
+ value[:jsonapi_type] = resource.type if resource
494
+ if interface
495
+ value[:interface] = true
496
+ value[:implementers] = []
497
+ end
498
+ type_registry[name] = value
499
+ end
500
+
501
+ def implement(type_class, interface_config)
502
+ type_class.implements(interface_config[:type])
503
+ interface_config[:implementers] << type_class
504
+ end
505
+
506
+ # Define interface for polymorphic_belongs_to sideload
507
+ # After defining, ensure child resources implement the interface
508
+ def interface_for_pbt(resource, sideload)
509
+ type_name = "#{resource.graphql_class_name}__#{sideload.name}"
510
+ interface = type_registry[type_name]
511
+ if !interface
512
+ klass = Module.new
513
+ klass.send :include, self.class.base_interface
514
+ klass.field :id, String, null: false
515
+ klass.field :_type, String, null: false
516
+ klass.graphql_name type_name
517
+ sideload.child_resources.each do |r|
518
+ type_registry[r.graphql_class_name][:type].implements(klass)
519
+ end
520
+ register(type_name, klass)
521
+ interface = klass
522
+ end
523
+ interface
524
+ end
525
+
526
+ class PageType < GraphQL::Schema::InputObject
527
+ graphql_name "Page"
528
+ argument :size, Int, required: false
529
+ argument :number, Int, required: false
530
+ end
531
+
532
+ class SortDirType < GraphQL::Schema::Enum
533
+ graphql_name "SortDir"
534
+ value "asc", "Ascending"
535
+ value "desc", "Descending"
536
+ end
537
+ end
538
+ end