lib/graphql/schema/visibility.rb in graphql-2.4.3 vs lib/graphql/schema/visibility.rb in graphql-2.4.4

- old
+ new

@@ -1,8 +1,9 @@ # frozen_string_literal: true require "graphql/schema/visibility/profile" require "graphql/schema/visibility/migration" +require "graphql/schema/visibility/visit" module GraphQL class Schema # Use this plugin to make some parts of your schema hidden from some viewers. # @@ -11,44 +12,80 @@ # @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles # @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`) # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails) ? Rails.env.production? : nil), migration_errors: false) schema.visibility = self.new(schema, dynamic: dynamic, preload: preload, profiles: profiles, migration_errors: migration_errors) + if preload + schema.visibility.preload + end end def initialize(schema, dynamic:, preload:, profiles:, migration_errors:) @schema = schema schema.use_visibility_profile = true - if migration_errors - schema.visibility_profile_class = Migration + schema.visibility_profile_class = if migration_errors + Visibility::Migration + else + Visibility::Profile end @preload = preload @profiles = profiles @cached_profiles = {} @dynamic = dynamic @migration_errors = migration_errors - if preload - # Traverse the schema now (and in the *_configured hooks below) - # To make sure things are loaded during boot - @preloaded_types = Set.new - types_to_visit = [ - @schema.query, - @schema.mutation, - @schema.subscription, - *@schema.introspection_system.types.values, - *@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap }, - *@schema.orphan_types, - ] - # Root types may have been nil: - types_to_visit.compact! - ensure_all_loaded(types_to_visit) + # Top-level type caches: + @visit = nil + @interface_type_memberships = nil + @directives = nil + @types = nil + @references = nil + @loaded_all = false + end - profiles.each do |profile_name, example_ctx| - example_ctx[:visibility_profile] = profile_name - prof = profile_for(example_ctx, profile_name) - prof.all_types # force loading - end + def all_directives + load_all + @directives + end + + def all_interface_type_memberships + load_all + @interface_type_memberships + end + + def all_references + load_all + @references + end + + def get_type(type_name) + load_all + @types[type_name] + end + + def preload? + @preload + end + + def preload + # Traverse the schema now (and in the *_configured hooks below) + # To make sure things are loaded during boot + @preloaded_types = Set.new + types_to_visit = [ + @schema.query, + @schema.mutation, + @schema.subscription, + *@schema.introspection_system.types.values, + *@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap }, + *@schema.orphan_types, + ] + # Root types may have been nil: + types_to_visit.compact! + ensure_all_loaded(types_to_visit) + @profiles.each do |profile_name, example_ctx| + example_ctx[:visibility_profile] = profile_name + prof = profile_for(example_ctx, profile_name) + prof.all_types # force loading end end # @api private def query_configured(query_type) @@ -130,19 +167,24 @@ else @schema.visibility_profile_class.new(context: context, schema: @schema) end end - private + attr_reader :top_level + # @api private + attr_reader :unfiltered_interface_type_memberships + def top_level_profile(refresh: false) if refresh @top_level_profile = nil end @top_level_profile ||= @schema.visibility_profile_class.new(context: Query::NullContext.instance, schema: @schema) end + private + def ensure_all_loaded(types_to_visit) while (type = types_to_visit.shift) if type.kind.fields? && @preloaded_types.add?(type) type.all_field_definitions.each do |field_defn| field_defn.ensure_loaded @@ -150,9 +192,89 @@ end end end top_level_profile(refresh: true) nil + end + + def load_all(types: nil) + if @visit.nil? + # Set up the visit system + @interface_type_memberships = Hash.new { |h, interface_type| h[interface_type] = [] }.compare_by_identity + @directives = [] + @types = {} # String => Module + @references = Hash.new { |h, member| h[member] = [] }.compare_by_identity + @unions_for_references = Set.new + @visit = Visibility::Visit.new(@schema) do |member| + if member.is_a?(Module) + type_name = member.graphql_name + if (prev_t = @types[type_name]) + if prev_t.is_a?(Array) + prev_t << member + else + @types[type_name] = [member, prev_t] + end + else + @types[member.graphql_name] = member + end + if member < GraphQL::Schema::Directive + @directives << member + elsif member.respond_to?(:interface_type_memberships) + member.interface_type_memberships.each do |itm| + @references[itm.abstract_type] << member + @interface_type_memberships[itm.abstract_type] << itm + end + elsif member < GraphQL::Schema::Union + @unions_for_references << member + end + elsif member.is_a?(GraphQL::Schema::Argument) + member.validate_default_value + @references[member.type.unwrap] << member + elsif member.is_a?(GraphQL::Schema::Field) + @references[member.type.unwrap] << member + end + true + end + + @schema.root_types.each do |t| + @references[t] << true + end + + @schema.introspection_system.types.each_value do |t| + @references[t] << true + end + @visit.visit_each(types: []) # visit default directives + end + + if types + @visit.visit_each(types: types, directives: []) + elsif @loaded_all == false + @loaded_all = true + @visit.visit_each + else + # already loaded all + return + end + + # TODO: somehow don't iterate over all these, + # only the ones that may have been modified + @interface_type_memberships.each do |int_type, type_memberships| + referers = @references[int_type].select { |r| r.is_a?(GraphQL::Schema::Field) } + if referers.any? + type_memberships.each do |type_membership| + implementor_type = type_membership.object_type + # Add new items only: + @references[implementor_type] |= referers + end + end + end + + @unions_for_references.each do |union_type| + refs = @references[union_type] + union_type.all_possible_types.each do |object_type| + @references[object_type] |= refs # Add new items + end + end end end end end