Apollo Connectors Preview Features
Want to try out the latest and greatest features of Apollo Connectors? You're in the right place!
Before you jump in, there are a couple important pieces of information:
All of these features are subject to change, use care when updating composition or GraphOS Router.
Make sure to tell us what you think before these features are stable!
Enabling Preview Features
Use GraphOS Router
2.3.0-preview.0
or greaterConfigure the router to allow the preview version of connectors:
1connectors:
2 preview_connect_v0_2: true
Use composition
2.11.0-preview.2
or greater withrover
and the Federation Next build pipeline in Studio.Update the schema to use the preview version of Connectors.
1extend schema
2 @link(
3 url: "https://specs.apollo.dev/connect/v0.2"
4 import: ["@source", "@connect"]
5 )
The order of these steps matters! Older versions of router will not accept the configuration setting, and routers without the configuration will reject schemas composed with the latest composition version.
@connect
on types
You can now apply the @connect
directive to a type!
1type Product
2 @connect(
3 source: "myApi"
4 http: { GET: "/products/{$this.id}" }
5 selection: "id name price"
6 ) {
7 id: ID!
8 name: String
9 price: String
10}
This works just like an entity: true
field on Query
, but with no field on query!
If you want to add extra fields to a type (via entities), and don't need the root field, this is the way to go.
$batch
for avoiding N+1
If you have a REST endpoint which can accept a list of IDs and return a list of objects, you can use the $batch
variable to avoid the N+1 problem.
$batch
is only available in Connectors that are on types (see above).
Previously, if you had a schema like this:
1type Query {
2 product(id: ID!): Product
3 @connect(
4 source: "myApi"
5 http: { GET: "/product/{$args.id}" }
6 selection: "id name reviews { id }"
7 )
8 review(id: ID!): Review!
9 @connect(
10 source: "myApi"
11 http: { GET: "/reviews/{$args.id}" }
12 selection: "id text rating"
13 entity: true
14 )
15}
16
17type Product {
18 id: ID!
19 name: String
20 price: String
21 reviews: [Review!]!
22}
23
24type Review {
25 id: ID!
26 text: String!
27 rating: Int!
28}
The /reviews
endpoint would be called once for each review ID on the product.
If you have an endpoint that resolves multiple reviews, you can instead do something like this:
1type Query {
2 product(id: ID!): Product
3 @connect(
4 source: "myApi"
5 http: { GET: "/product/{$args.id}" }
6 selection: "id name reviews { id }"
7 )
8}
9
10type Product {
11 id: ID!
12 name: String
13 price: String
14 reviews: [Review!]!
15}
16
17type Review
18 @connect(
19 source: "myApi"
20 http: { POST: "/reviews", body: "ids: $batch.id" }
21 selection: "id text rating"
22 ) {
23 id: ID!
24 text: String!
25 rating: Int!
26}
$batch
works just like $this
, but it is always an array of the objects being resolved, rather than a single value.
In this example, we use a JSON body to pass the list of IDs to the endpoints, but you can also use queryParams
Limiting batch sizes
When using the $batch
variable, you can limit how many IDs are sent in a single request using batch.maxSize
:
1type Review
2 @connect(
3 source: "myApi"
4 http: { POST: "/reviews", body: "ids: $batch.id" }
5 batch: { maxSize: 5 } # If there are more than 5 reviews, they will be split into multiple requests
6 selection: "id text rating"
7 ) {
8 id: ID!
9 text: String!
10 rating: Int!
11}
More ways to build URLs
More flexible templates
Some restrictions on the templates used in @connect
URLs have been lifted, allowing expressions in a few more places. For example:
1type Query {
2 products(filterName: String, filterValue: String): [Product]
3 @connect(
4 source: "myApi"
5 http: { GET: "/products?{$args.filterName}={$args.filterValue}" }
6 selection: "id name reviews { id }"
7 )
8}
Setting the query parameter name was not previously allowed, only the value.
Dynamic expressions will still always be percent-encoded and are still not allowed to modify the domain of the URL.
Method for comma-separated values
The new ->joinNotNull
method allows you to join a list of values with a comma or other characters, and will ignore any null values in the list.
1type Query {
2 products(filterName: String, filterValue: [String]): [Product]
3 @connect(
4 source: "myApi"
5 http: {
6 GET: "/products?{$args.filterName}={$args.filterValue->joinNotNull}"
7 }
8 selection: "id name reviews { id }"
9 )
10}
This is particularly useful for passing lists of IDs to a REST endpoint for batch requests to solve the N+1 problem.
1type Review
2 @connect(
3 source: "myApi"
4 http: { GET: "/reviews?ids={$batch.id->joinNotNull(',')}" }
5 selection: "id text rating"
6 ) {
7 id: ID!
8 text: String!
9 rating: Int!
10}
Adding multiple path parameters dynamically
The new http.path
variable is available in both @source
and @connect
, and allows appending multiple path parameters to the URL.
It uses the same mapping expressions as other parts of Connectors.
1extend schema
2 @source(
3 name: "myApi"
4 http: { baseURL: "http://example.com", path: "$config.pathComponents" }
5 )
6
7type Query {
8 products(pathComponents: [String!]!): [Product]
9 @connect(
10 source: "myApi"
11 http: { GET: "/products", path: "$args.pathComponents" }
12 selection: "id name reviews { id }"
13 )
14}
Path components will be appended starting with @source(http.baseURL)
, then @source(http.path)
, then the @connect
template, and finally @connect(http.path)
.
Expressions in http.path
must evaluate to arrays of scalars. Each value in that array will be percent-encoded and appended with a new slash.
The example above might result in a URL like http://example.com/from/config/products/from/arguments
.
Adding multiple query parameters dynamically
The new http.queryParams
attribute works much like http.path
, but must evaluate to an object where each key is a query parameter name and each value is
a query parameter value or list of values. Query parameters are appended (not overridden) in the same order as path segments:
Literal parameters in
baseURL
Dynamic parameters (for example, from
$config
) in@source(http.queryParams)
Query parameters from the connect template (static or from
{ }
expressions)Dynamic parameters in
@connect(http.queryParams)
The last of those is especially important for batching:
1type Review
2 @connect(
3 source: "myApi"
4 http: { GET: "/reviews", queryParams: "id: $batch.id" }
5 selection: "id text rating"
6 ) {
7 id: ID!
8 text: String!
9 rating: Int!
10}
Because $batch.id
is an array of IDs, the resulting URL will have multiple id
query parameters, like http://example.com/reviews?id=1&id=2&id=3
.
Customizing top-level errors
The new errors
argument in @source
and @connect
allows you to customize the top-level error message returned when a call from the router to a REST API fails. The errors.message
argument is a mapping expression that evaluates to a string, and the errors.extensions
argument is a mapping expression that evaluates to an object.
1extend schema
2 @source(
3 name: "myApi"
4 http: { baseURL: "http://example.com" }
5 errors: {
6 message: "error.details.localizedMessage"
7 extensions: """
8 code: error.details.code
9 """
10 }
11 )
1{
2 "errors": [
3 {
4 "message": "The API returned an error",
5 "extensions": {
6 "code": "INVALID_ARGUMENT"
7 }
8 }
9 ]
10}
$request.headers
and $response.headers
variables
The new $request.headers
and $response.headers
variables allow you to access the request and response headers in your mapping expressions.
1type Query {
2 products: [Product]
3 @connect(
4 source: "myApi"
5 http: { GET: "/products?foo={$request.headers.'x-foo'->first}" }
6 selection: """
7 id
8 name
9 bar: $response.headers.'x-bar'->first
10 """
11 )
12}
Note that headers are always lists, so in most cases you'll want to use ->first
to get the first value.