Skip to content

Commit 433f831

Browse files
authored
chore: add visibility snippet (#74)
* chore: add visibility example * test: add tests * test: use api key
1 parent a5b58db commit 433f831

File tree

6 files changed

+245
-0
lines changed

6 files changed

+245
-0
lines changed

protection/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ For more information on protecting your API, [see our documentation](https://www
77
- [makeAllPublic](makeAllPublic) shows how you can easily make all `Query` fields public, thus resulting in a public endpoint.
88
- [makeSomePublic](makeSomePublic) shows how you can make fields public, and some private (which can still be accessed using your `admin` or `service` keys).
99
- [simpleABACSample](simpleABACSample) shows how to control access to fields using JWT claims.
10+
- [visbility](visibility) shows use of schema controlled visibility of fields.

protection/visibility/README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Visibility
2+
3+
## Overview
4+
5+
Visibility patterns allows fine-grained control over fields that are exposed by a schema, including introspection and handling requests.
6+
7+
With directives `@materializer`, `@sequence`, `@supplies` and `@inject` fields can be resolved through resolution of other fields.
8+
9+
For example, here the field `Customer.orders` is resolved by the resolution of `Query.orders`, using `@materializer`.
10+
11+
```graphql
12+
type Query {
13+
customer(email: String!): Customer @rest(endpoint: "...")
14+
15+
orders(customer_id: ID!): [Order] @dbquery(type: "postgresql")
16+
}
17+
18+
type Customer {
19+
id: ID!
20+
name: String
21+
email: String
22+
orders: [Order]
23+
@materializer(
24+
query: "orders"
25+
arguments: { name: "customer_id", field: "id" }
26+
)
27+
}
28+
```
29+
30+
(details for `@rest`, `@dbquery` omitted for clarity)
31+
32+
In this case, we assume that the schema developer only wants to have clients obtain customer information through `Query.customer`,
33+
including their orders, that is they want to only have clients use the _graph_ defined by the schema (customers have orders).
34+
35+
This means they do not want clients to make a request such as `{orders(customer_id:1) {date cost}}`, instead only
36+
obtain orders through `{customer(email:"[email protected]") {name orders {date cost}}}`.
37+
38+
While this can be achieved by field access policies, visibility provides a scoping mechanism for fields within the schema definition (`*.graphql` files) itself.
39+
40+
And fields hidden by visibility are effectively not part of the external GraphQL schema and thus cannot be selected by a request
41+
or inspected using GraphQL introspection.
42+
43+
Visibiilty is applied before field access policies, as field access policies apply to the external schema of an endpoint.
44+
45+
> [!WARNING]
46+
> Visibility patterns are not applied for requests make with an admin key, thus the full schema definition from the `*.graphql` files is exposed. This is to aid debugging of schemas.
47+
48+
## Visibility patterns
49+
50+
Visibility is controlled through the directive argument `@sdl(visibility:)`.
51+
52+
The visibility patterns apply only to the schema elements that are included through `@sdl(files:)`.
53+
54+
For our example above we assume the schema is in `customer.graphl`, thus our `index.graphql` would look like:
55+
56+
```graphql
57+
schema
58+
@sdl(
59+
files: "customer.graphql"
60+
visibility: { expose: true, types: "Query", fields: "customer" }
61+
) {
62+
query: Query
63+
}
64+
```
65+
66+
Fields that match the pattern are defined using regular expressions in `types` and `fields`, that match type names and field names.
67+
68+
Defaults match the style of field access policies in that:
69+
70+
- Root operation type fields (`Query`, `Mutation`, `Subscription`) are not exposed by default.
71+
- All other fields in object and interface types are exposed by default.
72+
73+
Thus in this simple example all fields in `Query` are not exposed with the exception of `Query.customer`.
74+
75+
The external schema will only include `Query.customer` and and schema elements reachable from that field.
76+
77+
> [!NOTE]
78+
> Any fields defined in this `index.graphql` are **not** subject to the visibility patterns, as patterns only apply to the schema elements that are included through files listed in `@sdl(files:)`.
79+
80+
## Consistent field naming
81+
82+
Visibility patterns encourage a consistent naming policy for a GraphQL schema.
83+
For example using the prefix `_` for any "internal" field not to be exposed, can be enforced using a visibility pattern such as:
84+
85+
```graphql
86+
schema
87+
@sdl(
88+
files: "customer.graphql"
89+
visibility: [
90+
{ expose: true, types: "Query", fields: "customer" }
91+
{ expose: false, types: ".*", fields: "_.*" } # Any type, any field whose name starts with _
92+
]
93+
) {
94+
query: Query
95+
}
96+
```
97+
98+
> [!TIP]
99+
> Double underscore `__` as a prefix is reserved for GraphQL introspection and is not allowed.
100+
101+
## Try it out
102+
103+
Deploy the schema in this folder and then introspect the schema.
104+
105+
This lists the fields in `Query`
106+
107+
```graphql
108+
query {
109+
__schema {
110+
queryType {
111+
fields {
112+
name
113+
}
114+
}
115+
}
116+
}
117+
```
118+
119+
The response is, showing `Query.orders` is not visible:
120+
121+
```json
122+
{
123+
"data": {
124+
"__schema": {
125+
"description": "",
126+
"queryType": {
127+
"fields": [
128+
{
129+
"name": "customer"
130+
}
131+
]
132+
}
133+
}
134+
}
135+
}
136+
```
137+
138+
You can verify the `Query.orders` cannot be selected:
139+
140+
```graphql
141+
query {
142+
orders(customer_id: 1) {
143+
date
144+
when
145+
}
146+
}
147+
```
148+
149+
results in:
150+
151+
```json
152+
{
153+
"errors": [
154+
{
155+
"message": "Cannot query field \"orders\" on type \"Query\".",
156+
"locations": [
157+
{
158+
"line": 1,
159+
"column": 9
160+
}
161+
]
162+
}
163+
]
164+
}
165+
```
166+
167+
> [!NOTE]
168+
> If you see `Query.orders` then check if you are using the admin key in your request.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
type Query {
2+
customer(email: String!): Customer
3+
4+
orders(customer_id: ID!): [Order]
5+
}
6+
7+
type Customer @mock {
8+
id: ID!
9+
name: String! @mockfn(name: "LastName")
10+
email: String!
11+
orders: [Order]
12+
@materializer(
13+
query: "orders"
14+
arguments: { name: "customer_id", field: "id" }
15+
)
16+
}
17+
18+
type Order @mock {
19+
date: Date! @mockfn(name: "PastDate", values: 5)
20+
cost: Int! @mockfn(name: "NumberRange", values: [1, 500])
21+
}

protection/visibility/index.graphql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
schema
2+
@sdl(
3+
files: "customer.graphql"
4+
visibility: { expose: true, types: "Query", fields: "customer" }
5+
) {
6+
query: Query
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"endpoint": "api/miscellaneous"
3+
}

protection/visibility/tests/Test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const {
2+
deployAndRun,
3+
stepzen,
4+
getTestDescription,
5+
} = require("../../../tests/gqltest.js");
6+
7+
testDescription = getTestDescription("snippets", __dirname);
8+
9+
describe(testDescription, function () {
10+
const tests = [
11+
{
12+
label: "customer",
13+
query:
14+
'{customer(email:"[email protected]") {id name email orders {cost}}}',
15+
expected: {
16+
customer: {
17+
id: "464979",
18+
name: "Lesch",
19+
20+
orders: [
21+
{
22+
cost: 100,
23+
},
24+
],
25+
},
26+
},
27+
},
28+
{
29+
label: "query-fields",
30+
query: "{__schema {queryType {fields {name}}}}",
31+
expected: {
32+
__schema: {
33+
queryType: {
34+
fields: [
35+
{
36+
name: "customer",
37+
},
38+
],
39+
},
40+
},
41+
},
42+
},
43+
];
44+
return deployAndRun(__dirname, tests, stepzen.regular);
45+
});

0 commit comments

Comments
 (0)