class GraphQL::Schema::Visibility::Profile
-
It can be used to cache profiles by name for re-use across queries
- It checks ‘.visible?` on root introspection types
- It doesn’t hide Interface or Union types when all their possible types are hidden. (Instead, those types should implement ‘.visible?` to hide in that case.)
- It doesn’t use {Schema}‘s top-level caches (eg {Schema.references_to}, {Schema.possible_types}, {Schema.types})
It’s like {Warden}, but has some differences:
based on the given ‘context`.
This class filters the types, fields, arguments, enum values, and directives in a schema
def self.from_context(ctx, schema)
-
(Schema::Visibility::Profile)
-
def self.from_context(ctx, schema) if ctx.respond_to?(:types) && (types = ctx.types).is_a?(self) types else schema.visibility.profile_for(ctx) end end
def self.null_profile(context:, schema:)
def self.null_profile(context:, schema:) profile = self.new(name: "NullProfile", context: context, schema: schema) profile.instance_variable_set(:@cached_visible, Hash.new { |k, v| k[v] = true }.compare_by_identity) profile end
def all_types
def all_types load_all_types @all_types.values end
def all_types_h
def all_types_h load_all_types @all_types end
def argument(owner, arg_name)
def argument(owner, arg_name) arg = @cached_parent_arguments[owner][arg_name] if arg.is_a?(Array) visible_arg = nil arg.each do |arg_defn| if @cached_visible_arguments[owner][arg_defn] if visible_arg.nil? visible_arg = arg_defn else raise_duplicate_definition(visible_arg, arg_defn) end end end visible_arg else if arg && @cached_visible_arguments[owner][arg] arg else nil end end end
def arguments(owner)
def arguments(owner) @cached_arguments[owner] end
def directive_exists?(dir_name)
def directive_exists?(dir_name) directives.any? { |d| d.graphql_name == dir_name } end
def directives
def directives @all_directives ||= @visibility.all_directives.select { |dir| @cached_visible[dir] && @visibility.all_references[dir].any? { |ref| ref == true || (@cached_visible[ref] && referenced?(ref)) } } end
def enum_values(owner)
def enum_values(owner) @cached_enum_values[owner] end
def field(owner, field_name)
def field(owner, field_name) f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name]) field elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name)) entry_point_field elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name)) dynamic_field else nil end if f.is_a?(Array) visible_f = nil f.each do |f_defn| if @cached_visible_fields[owner][f_defn] if visible_f.nil? visible_f = f_defn else raise_duplicate_definition(visible_f, f_defn) end end end visible_f&.ensure_loaded elsif f && @cached_visible_fields[owner][f.ensure_loaded] f else nil end end
def field_on_visible_interface?(field, owner)
def field_on_visible_interface?(field, owner) ints = owner.interface_type_memberships.map(&:abstract_type) field_name = field.graphql_name filtered_ints = interfaces(owner) any_interface_has_field = false any_interface_has_visible_field = false ints.each do |int_t| if (_int_f_defn = @cached_parent_fields[int_t][field_name]) any_interface_has_field = true if filtered_ints.include?(int_t) # TODO cycles, or maybe not necessary since previously checked? && @cached_visible_fields[owner][field] any_interface_has_visible_field = true break end end end if any_interface_has_field any_interface_has_visible_field else true end end
def fields(owner)
def fields(owner) @cached_fields[owner] end
def freeze
def freeze @cached_visible.default_proc = nil @cached_visible_fields.default_proc = nil @cached_visible_fields.each do |type, fields| fields.default_proc = nil end @cached_visible_arguments.default_proc = nil @cached_visible_arguments.each do |type, fields| fields.default_proc = nil end @cached_parent_fields.default_proc = nil @cached_parent_fields.each do |type, fields| fields.default_proc = nil end @cached_parent_arguments.default_proc = nil @cached_parent_arguments.each do |type, args| args.default_proc = nil end @cached_possible_types.default_proc = nil @cached_enum_values.default_proc = nil @cached_fields.default_proc = nil @cached_arguments.default_proc = nil @loadable_possible_types.default_proc = nil super end
def initialize(name: nil, context:, schema:, visibility:)
def initialize(name: nil, context:, schema:, visibility:) @name = name @context = context @schema = schema @visibility = visibility @all_types = {} @all_types_loaded = false @unvisited_types = [] @all_directives = nil @cached_visible = Hash.new { |h, member| h[member] = @schema.visible?(member, @context) }.compare_by_identity @cached_visible_fields = Hash.new { |h, owner| h[owner] = Hash.new do |h2, field| h2[field] = visible_field_for(owner, field) end.compare_by_identity }.compare_by_identity @cached_visible_arguments = Hash.new do |h, owner| h[owner] = Hash.new do |h2, arg| h2[arg] = if @cached_visible[arg] && (arg_type = arg.type.unwrap) && @cached_visible[arg_type] case owner when GraphQL::Schema::Field @cached_visible_fields[owner.owner][owner] when Class @cached_visible[owner] else raise "Unexpected argument owner for `#{arg.path}`: #{owner.inspect}" end else false end end.compare_by_identity end.compare_by_identity @cached_parent_fields = Hash.new do |h, type| h[type] = Hash.new do |h2, field_name| h2[field_name] = type.get_field(field_name, @context) end end.compare_by_identity @cached_parent_arguments = Hash.new do |h, arg_owner| h[arg_owner] = Hash.new do |h2, arg_name| h2[arg_name] = arg_owner.get_argument(arg_name, @context) end end.compare_by_identity @cached_possible_types = Hash.new { |h, type| h[type] = possible_types_for(type) }.compare_by_identity @cached_enum_values = Hash.new do |h, enum_t| values = non_duplicate_items(enum_t.enum_values(@context), @cached_visible) if values.size == 0 raise GraphQL::Schema::Enum::MissingValuesError.new(enum_t) end h[enum_t] = values end.compare_by_identity @cached_fields = Hash.new do |h, owner| h[owner] = non_duplicate_items(owner.all_field_definitions.each(&:ensure_loaded), @cached_visible_fields[owner]) end.compare_by_identity @cached_arguments = Hash.new do |h, owner| h[owner] = non_duplicate_items(owner.all_argument_definitions, @cached_visible_arguments[owner]) end.compare_by_identity @loadable_possible_types = Hash.new { |h, union_type| h[union_type] = union_type.possible_types }.compare_by_identity end
def interfaces(obj_or_int_type)
def interfaces(obj_or_int_type) ints = obj_or_int_type.interface_type_memberships .select { |itm| @cached_visible[itm] && @cached_visible[itm.abstract_type] } .map!(&:abstract_type) ints.uniq! # Remove any duplicate interfaces implemented via other interfaces ints end
def load_all_types
def load_all_types return if @all_types_loaded @all_types_loaded = true visit = Visibility::Visit.new(@schema) do |member| if member.is_a?(Module) && member.respond_to?(:kind) if @cached_visible[member] && referenced?(member) type_name = member.graphql_name if (prev_t = @all_types[type_name]) && !prev_t.equal?(member) raise_duplicate_definition(member, prev_t) end @all_types[type_name] = member true else false end else @cached_visible[member] end end visit.visit_each @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) } nil end
def loadable?(t, _ctx)
def loadable?(t, _ctx) @cached_visible[t] && !referenced?(t) end
def loadable_possible_types(t, _ctx)
def loadable_possible_types(t, _ctx) @loadable_possible_types[t] end
def loaded_types
def loaded_types @all_types.values end
def mutation_root
def mutation_root ((t = @schema.mutation) && @cached_visible[t]) ? t : nil end
def non_duplicate_items(definitions, visibility_cache)
def non_duplicate_items(definitions, visibility_cache) non_dups = [] names = Set.new definitions.each do |defn| if visibility_cache[defn] if !names.add?(defn.graphql_name) dup_defn = non_dups.find { |d| d.graphql_name == defn.graphql_name } raise_duplicate_definition(dup_defn, defn) end non_dups << defn end end non_dups end
def possible_types(type)
def possible_types(type) @cached_possible_types[type] end
def possible_types_for(type)
def possible_types_for(type) case type.kind.name when "INTERFACE" pts = [] @visibility.all_interface_type_memberships[type].each do |impl_type, type_memberships| if impl_type.kind.object? && referenced?(impl_type) && @cached_visible[impl_type] if type_memberships.any? { |itm| @cached_visible[itm] } pts << impl_type end end end pts when "UNION" pts = [] type.type_memberships.each { |tm| if @cached_visible[tm] && (ot = tm.object_type) && @cached_visible[ot] && referenced?(ot) pts << ot end } pts when "OBJECT" if @cached_visible[type] [type] else EmptyObjects::EMPTY_ARRAY end else GraphQL::EmptyObjects::EMPTY_ARRAY end end
def preload
def preload load_all_types @all_types.each do |type_name, type_defn| if type_defn.kind.fields? fields(type_defn).each do |f| field(type_defn, f.graphql_name) arguments(f).each do |arg| argument(f, arg.graphql_name) end end @schema.introspection_system.dynamic_fields.each do |f| field(type_defn, f.graphql_name) end elsif type_defn.kind.input_object? arguments(type_defn).each do |arg| argument(type_defn, arg.graphql_name) end elsif type_defn.kind.enum? enum_values(type_defn) end # Lots more to do here end @schema.introspection_system.entry_points.each do |f| arguments(f).each do |arg| argument(f, arg.graphql_name) end field(@schema.query, f.graphql_name) end @schema.introspection_system.dynamic_fields.each do |f| arguments(f).each do |arg| argument(f, arg.graphql_name) end end end
def query_root
def query_root ((t = @schema.query) && @cached_visible[t]) ? t : nil end
def raise_duplicate_definition(first_defn, second_defn)
def raise_duplicate_definition(first_defn, second_defn) raise DuplicateNamesError.new(duplicated_name: first_defn.path, duplicated_definition_1: first_defn.inspect, duplicated_definition_2: second_defn.inspect) end
def reachable_type?(type_name)
def reachable_type?(type_name) load_all_types !!@all_types[type_name] end
def referenced?(type_defn)
def referenced?(type_defn) @visibility.all_references[type_defn].any? do |ref| case ref when GraphQL::Schema::Argument @cached_visible_arguments[ref.owner][ref] when GraphQL::Schema::Field @cached_visible_fields[ref.owner][ref] when Module @cached_visible[ref] when true true end end end
def subscription_root
def subscription_root ((t = @schema.subscription) && @cached_visible[t]) ? t : nil end
def type(type_name)
def type(type_name) t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop if t if t.is_a?(Array) vis_t = nil t.each do |t_defn| if @cached_visible[t_defn] && referenced?(t_defn) if vis_t.nil? vis_t = t_defn else raise_duplicate_definition(vis_t, t_defn) end end end vis_t else if t && @cached_visible[t] && referenced?(t) t else nil end end end end
def visible_enum_value?(enum_value, _ctx = nil)
def visible_enum_value?(enum_value, _ctx = nil) @cached_visible[enum_value] end
def visible_field_for(owner, field)
def visible_field_for(owner, field) @cached_visible[field] && (ret_type = field.type.unwrap) && @cached_visible[ret_type] && (owner == field.owner || (!owner.kind.object?) || field_on_visible_interface?(field, owner)) end