import { Callout } from '@theguild/components'

# External Schema Composition

External Schema Composition allows you to build and validate GraphQL Schema outside of GraphQL Hive.
When enabled, GraphQL Hive will send necessary information over HTTP to your HTTP endpoint and expect a composition result in return.

You most likely don't need this feature, but in some rare cases, it can be useful.

The most common reasons for using the external schema composition are licensing and limited language support (GraphQL Hive runs on NodeJS).

## Configuration

To enable external schema composition:

1. Go to your project settings.
2. Find the **"External Composition"** section.
3. Click the toggle button.

![Disabled external schema composition](/external-schema-composition-disabled.png)

4. Provide the URL of your HTTP endpoint and the secret.
5. Click "Save"

![External schema composition form](/external-schema-composition-enabled.png)

## HTTP Endpoint

Your HTTP endpoint must be a POST endpoint that accepts JSON and returns JSON.

### Securing your endpoint

To make sure your server is only receiving requests from GraphQL Hive, you can use the secret provided in the configuration and verify the signature of the request.

> Please contact us if you wish to limit access by an IP address.

The logic of verifying the signature is as follows:

1. Get the value of `X-Hive-Signature-256` header.
1. Take the raw body of the request.
1. Use an HMAC hex digest (`sha256`) to compute the hash (body with the secret provided in the configuration).
1. Compare the result with the value of the `X-Hive-Signature-256` header (use "constant-time" comparison).

A NodeJS example of the verification process:

```javascript
import crypto from 'node:crypto'

const sigHashAlg = 'sha256'

function hash(secret: string, data: string) {
  return crypto.createHmac(sigHashAlg, secret).update(data, 'utf-8').digest('hex')
}

function verifyRequest(input) {
  const { body, signature, secret } = input

  if (!body) {
    return 'ERR_EMPTY_BODY'
  }

  const sig = Buffer.from(signature ?? '', 'utf8')
  const digest = Buffer.from(hash(secret, body), 'utf8')

  if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
    return 'ERR_INVALID_SIGNATURE'
  }

  // signature is valid
}
```

### Specification

Expected shape of data and samples.

<details>
  <summary>Request</summary>

The request body will contain the following information:

```typescript
type SchemaService = {
  sdl: String!
  name: String!
  url: String
}

type RequestBody = Array<SchemaService>
```

Example request:

```json
[
  {
    "sdl": "type Query { users: [String] }",
    "name": "users",
    "url": "https://api.com/users"
  },
  {
    "sdl": "extend type Query { comments: [String] }",
    "name": "comments",
    "url": "https://api.com/comments"
  }
]
```

</details>

<details>
  <summary>Response</summary>

The reponse payload should match the following type:

```typescript
type CompositionResult = CompositionSuccess | CompositionFailure

type CompositionSuccess = {
  type: 'success'
  result: {
    supergraph: string
    sdl: string
  }
}

type CompositionFailure = {
  type: 'failure'
  result: {
    errors: Array<{
      message: string
    }>
  }
}
```

Example response:

```json
{
  "type": "failure",
  "result": {
    "errors": [
      {
        "message": "Type \"Query\" was defined more than once."
      }
    ]
  }
}
```

</details>

### Apollo Federation v1 example

The following example shows how to implement an external composition endpoint for Apollo Federation v1 in NodeJS.

```bash
npm install @graphql-hive/external-composition
```

<Callout>
  The `@graphql-hive/external-composition` library provides a thin `compose` function to bring auto-completion and
  type-safety to your IDE.
</Callout>

```javascript
import { verifyRequest, compose, signatureHeaderName } from '@graphql-hive/external-composition'
import { composeAndValidate, compositionHasErrors } from '@apollo/federation'
import { parse, printSchema } from 'graphql'
import fastify from 'fastify'

const composeFederation = compose(services => {
  const result = composeAndValidate(
    services.map(service => {
      return {
        typeDefs: parse(service.sdl),
        name: service.name,
        url: service.url
      }
    })
  )

  if (compositionHasErrors(result)) {
    return {
      type: 'failure',
      result: {
        errors: result.errors.map(err => ({
          message: err.message
        }))
      }
    }
  } else {
    return {
      type: 'success',
      result: {
        supergraph: result.supergraphSdl,
        sdl: printSchema(result.schema)
      }
    }
  }
})

const server = fastify()

server.route({
  method: ['POST'],
  url: '/compose',
  handler(req, res) {
    const error = verifyRequest({
      // Stringified body, or raw body if you have access to it
      body: JSON.stringify(req.body),
      // Pass here the signature from `X-Hive-Signature-256` header
      signature: req.headers[signatureHeaderName],
      // Pass here the secret you configured in GraphQL Hive
      secret: YOUR_SECRET_HERE
    })

    if (error) {
      // Failed to verify the request - send 500 and the error message back
      res.status(500).send(error)
    } else {
      const result = composeFederation(req.body)
      // Send the result back (as JSON)
      res.send(JSON.stringify(result))
    }
  }
})

await server.listen({
  port: 3000
})
```

### Apollo Federation v2 example

The following example shows how to implement an external composition endpoint for Apollo Federation v2 in NodeJS.

```bash
npm install @graphql-hive/external-composition
```

<Callout>
  The `@graphql-hive/external-composition` library provides a thin `compose` function to bring auto-completion and
  type-safety to your IDE.
</Callout>

```javascript
import { verifyRequest, compose, signatureHeaderName } from '@graphql-hive/external-composition'
import { composeServices } from '@apollo/composition'
import { parse, printSchema } from 'graphql'
import fastify from 'fastify'

const composeFederation = compose(services => {
  const result = composeServices(
    services.map(service => {
      return {
        typeDefs: parse(service.sdl),
        name: service.name,
        url: service.url
      }
    })
  )

  if (result.errors?.length) {
    return {
      type: 'failure',
      result: {
        errors: result.errors.map(error => ({
          message: error.message
        }))
      }
    }
  } else {
    return {
      type: 'success',
      result: {
        supergraph: result.supergraphSdl,
        sdl: printSchema(result.schema.toGraphQLJSSchema())
      }
    }
  }
})

const server = fastify()

server.route({
  method: ['POST'],
  url: '/compose',
  handler(req, res) {
    const error = verifyRequest({
      // Stringified body, or raw body if you have access to it
      body: JSON.stringify(req.body),
      // Pass here the signature from `X-Hive-Signature-256` header
      signature: req.headers[signatureHeaderName],
      // Pass here the secret you configured in GraphQL Hive
      secret: YOUR_SECRET_HERE
    })

    if (error) {
      // Failed to verify the request - send 500 and the error message back
      res.status(500).send(error)
    } else {
      const result = composeFederation(req.body)
      // Send the result back (as JSON)
      res.send(JSON.stringify(result))
    }
  }
})

await server.listen({
  port: 3000
})
```
