lib/graphiti_graphql/federation.rb in graphiti_graphql-0.1.1 vs lib/graphiti_graphql/federation.rb in graphiti_graphql-0.1.2

- old
+ new

@@ -10,31 +10,32 @@ raise "You must add the 'graphql-batch' gem to use GraphitiGraphQL federation" end # We don't want to add these as dependencies, # but do need to check things don't break -if Gem::Version.new(ApolloFederation::VERSION) >= Gem::Version.new('2.0.0') +if Gem::Version.new(ApolloFederation::VERSION) >= Gem::Version.new("2.0.0") raise "graphiti_graphql federation is incompatible with apollo-federation >= 2" end -if Gem::Version.new(GraphQL::Batch::VERSION) >= Gem::Version.new('1.0.0') +if Gem::Version.new(GraphQL::Batch::VERSION) >= Gem::Version.new("1.0.0") raise "graphiti_graphql federation is incompatible with graphql-batch >= 1" end require "graphiti_graphql" +require "graphiti_graphql/federation/loaders/has_many" +require "graphiti_graphql/federation/loaders/belongs_to" +require "graphiti_graphql/federation/federated_resource" +require "graphiti_graphql/federation/federated_relationship" +require "graphiti_graphql/federation/resource_dsl" +require "graphiti_graphql/federation/apollo_federation_override" +require "graphiti_graphql/federation/schema_decorator" module GraphitiGraphQL module Federation - - def self.external_resources - @external_resources ||= {} - end - - def self.clear! - @external_resources = {} - end - + # * Extend Graphiti::Resource with federated_* macros + # * Add apollo-federation modules to graphql-ruby base types + # * Mark federation = true for checks down the line def self.setup! Graphiti::Resource.send(:include, ResourceDSL) schema = GraphitiGraphQL::Schema schema.base_field = Class.new(schema.base_field) do include ApolloFederation::Field @@ -48,216 +49,7 @@ include ApolloFederation::Interface end schema.base_interface.field_class(schema.base_field) GraphitiGraphQL::Schema.federation = true end - - class HasManyLoader < GraphQL::Batch::Loader - def initialize(resource_class, params, foreign_key) - @resource_class = resource_class - @params = params - @foreign_key = foreign_key - end - - def perform(ids) - @params[:filter] ||= {} - @params[:filter].merge!(@foreign_key => { eq: ids.join(",") }) - - if ids.length > 1 && @params[:page] - raise Graphiti::Errors::UnsupportedPagination - elsif !@params[:page] - @params[:page] = { size: 999 } - end - - Util.with_gql_context do - records = @resource_class.all(@params).as_json[:data] - fk = ->(record) { record[@foreign_key].to_s } - map = records.group_by(&fk) - ids.each do |id| - fulfill(id, (map[id] || [])) - end - end - end - end - - class BelongsToLoader < GraphQL::Batch::Loader - def initialize(resource_class, fields) - @resource_class = resource_class - @fields = fields - end - - def perform(ids) - Util.with_gql_context do - params = { filter: { id: { eq: ids.join(",") } } } - params[:fields] = { @resource_class.type => @fields.join(",") } - records = @resource_class.all(params).as_json[:data] - pk = ->(record) { record[:id].to_s } - map = records.index_by(&pk) - ids.each { |id| fulfill(id, map[id]) } - end - end - end - - class ExternalRelationship - attr_reader :name, :local_resource_class, :foreign_key - - def initialize(kind, name, local_resource_class, foreign_key) - @kind = kind - @name = name - @local_resource_class = local_resource_class - @foreign_key = foreign_key - end - - def has_many? - @kind == :has_many - end - - def belongs_to? - @kind == :belongs_to - end - end - - class ExternalResource - attr_reader :type_name, :relationships - - def initialize(type_name) - @type_name = type_name - @relationships = {} - end - - def add_relationship( - kind, - name, - local_resource_class, - foreign_key - ) - @relationships[name] = ExternalRelationship - .new(kind, name, local_resource_class, foreign_key) - end - end - - class TypeProxy - def initialize(caller, type_name) - @caller = caller - @type_name = type_name - end - - def has_many(relationship_name, foreign_key: nil) - @caller.federated_has_many relationship_name, - type: @type_name, - foreign_key: foreign_key - end - end - - module ResourceDSL - extend ActiveSupport::Concern - - class_methods do - def federated_type(type_name) - TypeProxy.new(self, type_name) - end - - # TODO: raise error if belongs_to doesn't have corresponding filter (on schema gen) - # TODO: hang these on the resource classes themselves - def federated_has_many(name, type:, foreign_key: nil) - foreign_key ||= :"#{type.underscore}_id" - resource = GraphitiGraphQL::Federation.external_resources[type] ||= - ExternalResource.new(type) - resource.add_relationship(:has_many, name, self, foreign_key) - - attribute = attributes.find do |name, config| - name.to_sym == foreign_key && !!config[:readable] && !!config[:filterable] - end - has_filter = filters.key?(foreign_key) - if !attribute && !has_filter - attribute foreign_key, :integer, - only: [:readable, :filterable], - schema: false, - readable: :gql?, - filterable: :gql? - elsif has_filter && !attribute - prior = filters[foreign_key] - attribute foreign_key, prior[:type], - only: [:readable, :filterable], - schema: false, - readable: :gql? - filters[foreign_key] = prior - elsif attribute && !has_filter - filter foreign_key, attribute[:type] - end - end - - def federated_belongs_to(name, type: nil, foreign_key: nil) - type ||= name.to_s.camelize - foreign_key ||= :"#{name.to_s.underscore}_id" - resource = GraphitiGraphQL::Federation.external_resources[type] ||= - ExternalResource.new(type) - resource.add_relationship(:belongs_to, name, self, foreign_key) - - attribute name, :hash, readable: :gql?, only: [:readable], schema: false do - fk = if prc = self.class.attribute_blocks[foreign_key] - instance_eval(&prc) - else - @object.send(foreign_key) - end - { - __typename: type, - id: fk.to_s - } - end - end - end - - def gql? - Graphiti.context[:graphql] - end - end end end - -# Hacky sack! -# All we're doing here is adding extras: [:lookahead] to the _entities field -# And passing to to the .resolve_reference method when arity is 3 -# This way we can request only fields the user wants when resolving the reference -# Important because we blow up when a field is guarded, and the guard fails -ApolloFederation::EntitiesField::ClassMethods.module_eval do - alias_method :define_entities_field_without_override, :define_entities_field - def define_entities_field(*args) - result = define_entities_field_without_override(*args) - extras = fields["_entities"].extras - extras |= [:lookahead] - fields["_entities"].instance_variable_set(:@extras, extras) - result - end -end - -module EntitiesFieldOverride - def _entities(representations:, lookahead:) # accept the lookahead as argument - representations.map do |reference| - typename = reference[:__typename] - type = context.warden.get_type(typename) - if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT - raise "The _entities resolver tried to load an entity for type \"#{typename}\"," \ - ' but no object type of that name was found in the schema' - end - - type_class = type.is_a?(GraphQL::ObjectType) ? type.metadata[:type_class] : type - if type_class.respond_to?(:resolve_reference) - meth = type_class.method(:resolve_reference) - # ** THIS IS OUR EDIT ** - result = if meth.arity == 3 - type_class.resolve_reference(reference, context, lookahead) - else - type_class.resolve_reference(reference, context) - end - else - result = reference - end - - context.schema.after_lazy(result) do |resolved_value| - context[resolved_value] = type - resolved_value - end - end - end -end -ApolloFederation::EntitiesField.send :prepend, EntitiesFieldOverride \ No newline at end of file