|
| 1 | +# Visibility |
| 2 | + |
| 3 | +Visibility controls can hide parts of a supergraph from select audiences without compromising stitching operations. Restricted schema elements are hidden from introspection and validate as though they do not exist (which is different from traditional authorization where an element is acknowledged as restricted). Visibility is useful for managing multiple distributions of a schema for different audiences. |
| 4 | + |
| 5 | +Under the hood, this system wraps `GraphQL::Schema::Visibility` (with nil profile support) and requires at least GraphQL Ruby v2.5.3. |
| 6 | + |
| 7 | +## Example |
| 8 | + |
| 9 | +Schemas may include a `@visibility` directive that defines element _profiles_. A profile is just a label describing an API distribution (public, private, etc). When a request is assigned a visibility profile, it can only access elements belonging to that profile. Elements without an explicit `@visibility` constraint belong to all profiles. For example: |
| 10 | + |
| 11 | +_schemas/product_info.graphql_ |
| 12 | +```graphql |
| 13 | +directive @stitch(key: String!) on FIELD_DEFINITION |
| 14 | +directive @visibility(profiles: [String!]!) on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM | SCALAR | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE |
| 15 | + |
| 16 | +type Product { |
| 17 | + id: ID! |
| 18 | + title: String! |
| 19 | + description: String! |
| 20 | +} |
| 21 | + |
| 22 | +type Query { |
| 23 | + featuredProduct: Product |
| 24 | + product(id: ID!): Product @stitch(key: "id") @visibility(profiles: ["private"]) |
| 25 | +} |
| 26 | +``` |
| 27 | + |
| 28 | +_schemas/product_prices.graphql_ |
| 29 | +```graphql |
| 30 | +directive @stitch(key: String!) on FIELD_DEFINITION |
| 31 | +directive @visibility(profiles: [String!]!) on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM | SCALAR | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE |
| 32 | + |
| 33 | +type Product { |
| 34 | + id: ID! @visibility(profiles: []) |
| 35 | + msrp: Float! @visibility(profiles: ["private"]) |
| 36 | + price: Float! |
| 37 | +} |
| 38 | + |
| 39 | +type Query { |
| 40 | + products(ids: [ID!]!): [Product]! @stitch(key: "id") @visibility(profiles: ["private"]) |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +When composing a stitching client, the names of all possible visibility profiles that the supergraph responds to should be specified in composer options: |
| 45 | + |
| 46 | +```ruby |
| 47 | +client = GraphQL::Stitching::Client.new( |
| 48 | + composer_options: { |
| 49 | + visibility_profiles: ["public", "private"], |
| 50 | + }, |
| 51 | + locations: { |
| 52 | + info: { |
| 53 | + schema: GraphQL::Schema.from_definition(File.read("schemas/product_info.graphql")), |
| 54 | + executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"), |
| 55 | + }, |
| 56 | + prices: { |
| 57 | + schema: GraphQL::Schema.from_definition(File.read("schemas/product_prices.graphql")), |
| 58 | + executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"), |
| 59 | + }, |
| 60 | + } |
| 61 | +) |
| 62 | +``` |
| 63 | + |
| 64 | +The client can then execute requests with a `visibility_profile` parameter in context that specifies the name of any profile the supergraph was composed with, or nil: |
| 65 | + |
| 66 | +```ruby |
| 67 | +query = %|{ |
| 68 | + featuredProduct { |
| 69 | + title # always visible |
| 70 | + price # always visible |
| 71 | + msrp # only visible to internal and nil profiles |
| 72 | + id # only visible to nil profile |
| 73 | + } |
| 74 | +}| |
| 75 | + |
| 76 | +result = client.execute(query, context: { |
| 77 | + visibility_profile: "public", # << or private, or nil |
| 78 | +}) |
| 79 | +``` |
| 80 | + |
| 81 | +The `visibility_profile` parameter will select which visibility distribution to use while introspecting and validating the request. For example: |
| 82 | + |
| 83 | +- Using `visibility_profile: "public"` will say the `msrp` field does not exist (because it is restricted to "private"). |
| 84 | +- Using `visibility_profile: "private"` will accesses the `msrp` field as usual. |
| 85 | +- Using `visibility_profile: nil` will access the entire graph without any visibility constraints. |
| 86 | + |
| 87 | +The full potential of visibility comes when hiding stitching implementation details, such as the `id` field (which is the stitching key for the Product type). While the `id` field is hidden from all named profiles, it remains operational for the stitching implementation. |
| 88 | + |
| 89 | +## Adding visibility directives |
| 90 | + |
| 91 | +Add the `@visibility` directive into schemas using the library definition: |
| 92 | + |
| 93 | +```ruby |
| 94 | +class QueryType < GraphQL::Schema::Object |
| 95 | + field :my_field, String, null: true do |f| |
| 96 | + f.directive(GraphQL::Stitching::Directives::Visibility, profiles: ["private"]) |
| 97 | + end |
| 98 | +end |
| 99 | + |
| 100 | +class MySchema < GraphQL::Schema |
| 101 | + directive(GraphQL::Stitching::Directives::Visibility) |
| 102 | + query(QueryType) |
| 103 | +end |
| 104 | +``` |
| 105 | + |
| 106 | +## Merging visibilities |
| 107 | + |
| 108 | +Visibility directives merge across schemas into the narrowest constraint possible. Profile sets for an element will intersect into its supergraph constraint: |
| 109 | + |
| 110 | +```graphql |
| 111 | +# location 1 |
| 112 | +myField: String @visibility(profiles: ["a", "c"]) |
| 113 | + |
| 114 | +# location 2 |
| 115 | +myField: String @visibility(profiles: ["b", "c"]) |
| 116 | + |
| 117 | +# merged supergraph |
| 118 | +myField: String @visibility(profiles: ["c"]) |
| 119 | +``` |
| 120 | + |
| 121 | +This may cause an element's profiles to intersect into an empty set, which means the element belongs to no profiles and will be hidden from all named distributions: |
| 122 | + |
| 123 | +```graphql |
| 124 | +# location 1 |
| 125 | +myField: String @visibility(profiles: ["a"]) |
| 126 | + |
| 127 | +# location 2 |
| 128 | +myField: String @visibility(profiles: ["b"]) |
| 129 | + |
| 130 | +# merged supergraph |
| 131 | +myField: String @visibility(profiles: []) |
| 132 | +``` |
| 133 | + |
| 134 | +Locations may omit visibility information to give other locations full control. Remember that elements without a `@visibility` constraint belong to all profiles, which also applies while merging: |
| 135 | + |
| 136 | +```graphql |
| 137 | +# location 1 |
| 138 | +myField: String |
| 139 | + |
| 140 | +# location 2 |
| 141 | +myField: String @visibility(profiles: ["b"]) |
| 142 | + |
| 143 | +# merged supergraph |
| 144 | +myField: String @visibility(profiles: ["b"]) |
| 145 | +``` |
| 146 | + |
| 147 | +## Type controls |
| 148 | + |
| 149 | +Visibility controls can be applied to almost all GraphQL schema elements, including: |
| 150 | + |
| 151 | +- Types (Object, Interface, Union, Enum, Scalar, InputObject) |
| 152 | +- Fields (of Object and Interface) |
| 153 | +- Arguments (of Field and InputObject) |
| 154 | +- Enum values |
| 155 | + |
| 156 | +While the visibility of type members (fields, arguments, and enum values) are pretty intuitive, the visibility of parent types is far more nuanced as constraints start to cascade: |
| 157 | + |
| 158 | +```graphql |
| 159 | +type Widget @visibility(profiles: ["private"]) { |
| 160 | + title: String |
| 161 | +} |
| 162 | + |
| 163 | +type Query { |
| 164 | + widget: Widget # << GETS HIDDEN |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +In this example, hiding the `Widget` type will also hide the `Query.widget` field that returns it. |
0 commit comments