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:

  1. All of these features are subject to change, use care when updating composition or GraphOS Router.

  2. Make sure to tell us what you think before these features are stable!

Enabling Preview Features

  1. Use GraphOS Router 2.3.0-preview.0 or greater

  2. Configure the router to allow the preview version of connectors:

YAML
1connectors:
2  preview_connect_v0_2: true
  1. Use composition 2.11.0-preview.2 or greater with rover and the Federation Next build pipeline in Studio.

  2. Update the schema to use the preview version of Connectors.

GraphQL
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!

GraphQL
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:

GraphQL
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:

GraphQL
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:

GraphQL
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:

GraphQL
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.

GraphQL
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.

GraphQL
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.

GraphQL
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:

  1. Literal parameters in baseURL

  2. Dynamic parameters (for example, from $config) in @source(http.queryParams)

  3. Query parameters from the connect template (static or from { } expressions)

  4. Dynamic parameters in @connect(http.queryParams)

The last of those is especially important for batching:

GraphQL
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.

GraphQL
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  )
JSON
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.

GraphQL
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.

Feedback

Ask Community