Skip to content

Commit

Permalink
Start documenting Visibility::Profile
Browse files Browse the repository at this point in the history
  • Loading branch information
rmosolgo committed Oct 8, 2024
1 parent 23c79bb commit 466a4e6
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 9 deletions.
59 changes: 58 additions & 1 deletion guides/authorization/visibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ Here are some reasons you might want to hide parts of your schema:

## Hiding Parts of the Schema

You can customize the visibility of parts of your schema by reimplementing various `visible?` methods:
To start limiting visibility of your schema, add the plugin:

```ruby
class MySchema < GraphQL::Schema
# ...
use GraphQL::Schema::Visibility # see below for options
end
```

Then, you can customize the visibility of parts of your schema by reimplementing various `visible?` methods:

- Type classes have a `.visible?(context)` class method
- Fields and arguments have a `#visible?(context)` instance method
Expand All @@ -30,6 +39,31 @@ These methods are called with the query context, based on the hash you pass as `
- In introspection, the member will _not_ be included in the result
- In normal queries, if a query references that member, it will return a validation error, since that member doesn't exist

## Visibility Profiles

You can use named profiles to cache your schema's visibility modes. For example:

```ruby
use GraphQL::Schema::Visibility, profiles: {
# mode_name => example_context_hash
public: { public: true },
beta: { public: true, beta: true },
internal_admin: { internal_admin: true }
}
```

Then, you can run queries with `context[:visibility_profile]` equal to one of the pre-defined profiles. When you do, GraphQL-Ruby will use a precomputed set of types and fields for that query.

### Preloading profiles

By default, GraphQL-Ruby will preload all named visibility profiles when `Rails.env.production?` is present and true. You can manually set this option by passing `use ... preload: true` (or `false`). Enable preloading in production to reduce latency of the first request to each visibility profile. Disable preloading in development to speed up application boot.

### Dynamic profiles

When you provide named visibility profiles, `context[:visibility_profile]` is required for query execution. You can also permit dynamic visibility for queries which _don't_ have that key set by passing `use ..., dynamic: true`. You could use this to support backwards compatibility or when visibility calculations are too complex to predefine.

When no named profiles are defined, all queries use dynamic visibility.

## Object Visibility

Let's say you're working on a new feature which should remain secret for a while. You can implement `.visible?` in a type:
Expand Down Expand Up @@ -107,3 +141,26 @@ end
```

For big schemas, this can be a worthwhile speed-up.

## Migration Notes

{% "GraphQL::Schema::Visibility" | api_doc %} is a _new_ implementation of visibility in GraphQL-Ruby. It has some slight differences from the previous implementation ({% "GraphQL::Schema::Warden" | api_doc %}):

- `Visibility` speeds up Rails app boot because it doesn't require all types to be loaded during boot and only loads types as they are used by queries.
- `Visibility` supports predefined, reusable visibility profiles which speeds up queries using complicated `visible?` checks.
- `Visibility` hides types differently in a few edge cases:
- Previously, `Warden` hide interface and union types which had no possible types. `Visibility` doesn't check possible types (in order to support performance improvements), so those types must return `false` for `visible?` in the same cases where all possible types were hidden. Otherwise, that interface or union type will be visible but have no possible types.
- Some other thing, see TODO
- When `Visibility` is used, several (Ruby-level) Schema introspection methods don't work because the caches they draw on haven't been calculated (`Schema.references_to`, `Schema.union_memberships`). If you're using these, please get in touch so that we can find a way forward.

### Migration Mode

You can use `use GraphQL::Schema::Visibility, ... migration_errors: true` to enable migration mode. In this mode, GraphQL-Ruby will make visibility checks with _both_ `Visibility` and `Warden` and compare the result, raising a descriptive error when the two systems return different results. As you migrate to `Visibility`, enable this mode in test to find any unexpected discrepancies.

Sometimes, there's a discrepancy that is hard to resolve but doesn't make any _real_ difference in application behavior. To address these cases, you can use these flags in `context`:

- `context[:visibility_migration_running] = true` is set in the main query context.
- `context[:visibility_migration_warden_running] = true` is set in the _duplicate_ context which is passed to a `Warden` instance.
- If you set `context[:skip_migration_error] = true`, then no migration error will be raised for that query.

You can use these flags to conditionally handle edge cases that should be ignored in testing.
5 changes: 4 additions & 1 deletion guides/schema/dynamic_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ desc: Using different schema members for each request
index: 8
---

You can use different versions of your GraphQL schema for each operation. To do this, implement `visible?(context)` on the parts of your schema that will be conditionally accessible. Additionally, many schema elements have definition methods which are called at runtime by GraphQL-Ruby. You can re-implement those to return any valid schema objects. GraphQL-Ruby caches schema elements for the duration of the operation, but if you're making external service calls to implement the methods below, consider adding a cache layer to improve the client experience and reduce load on your backend.
You can use different versions of your GraphQL schema for each operation. To do this, add `use GraphQL::Schema::Visibility` and implement `visible?(context)` on the parts of your schema that will be conditionally accessible. Additionally, many schema elements have definition methods which are called at runtime by GraphQL-Ruby. You can re-implement those to return any valid schema objects.


GraphQL-Ruby caches schema elements for the duration of the operation, but if you're making external service calls to implement the methods below, consider adding a cache layer to improve the client experience and reduce load on your backend.

At runtime, ensure that only one object is visible per name (type name, field name, etc.). (If `.visible?(context)` returns `false`, then that part of the schema will be hidden for the current operation.)

Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/schema/visibility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Visibility
# @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_ARRAY, preload: (defined?(Rails) ? Rails.env.production? : nil), migration_errors: false)
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)
end

Expand Down
6 changes: 1 addition & 5 deletions lib/graphql/schema/visibility/migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,9 @@ class Visibility
#
# @example Adding this plugin
#
# use GraphQL::Schema::Visibility::Migration
# use GraphQL::Schema::Visibility, migration_errors: true
#
class Migration < GraphQL::Schema::Visibility::Profile
def self.use(schema)
schema.visibility_profile_class = self
end

class RuntimeTypesMismatchError < GraphQL::Error
def initialize(method_called, warden_result, profile_result, method_args)
super(<<~ERR)
Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/schema/dynamic_members_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ class OtherObject < GraphQL::Schema::Object
field :f, Int, null: false
end

# TODO why is this necessary?
if GraphQL::Schema.use_visibility_profile?
ThingInterface.orphan_types(OtherObject)
end
Expand Down Expand Up @@ -934,7 +935,6 @@ def thing
end

query(Query)
use GraphQL::Schema::Visibility::Migration
end
end

Expand Down

0 comments on commit 466a4e6

Please sign in to comment.