# GraphQL

Source: https://apigenie.pl/docs/reference/server/graphql
Part of the reference of [apigenie-server](https://apigenie.pl/docs/reference/server.md).
Its other pages: [Responses](https://apigenie.pl/docs/reference/server/responses.md), [Scenarios and extensions](https://apigenie.pl/docs/reference/server/scenarios.md), [Scripts](https://apigenie.pl/docs/reference/server/scripts.md), [Document store](https://apigenie.pl/docs/reference/server/store.md), [Several services](https://apigenie.pl/docs/reference/server/services.md), [Validation and security](https://apigenie.pl/docs/reference/server/validation.md), [Contract testing (validating proxy)](https://apigenie.pl/docs/reference/server/contract-testing.md), [Session recording and replay](https://apigenie.pl/docs/reference/server/recording.md), [AsyncAPI](https://apigenie.pl/docs/reference/server/asyncapi.md), [Protobuf / gRPC](https://apigenie.pl/docs/reference/server/grpc.md), [Configuration](https://apigenie.pl/docs/reference/server/configuration.md), [Dashboard and admin API](https://apigenie.pl/docs/reference/server/dashboard.md), [TLS](https://apigenie.pl/docs/reference/server/tls.md).

A GraphQL schema is served as a GraphQL endpoint, `/graphql` under `--base-path`
(`graphql.path` changes it). Every query, mutation and subscription the schema allows is
answered, with no resolvers to write.

- **GraphQL over HTTP.** `POST` with `application/json` (`{query, variables, operationName,
  extensions}`) or `application/graphql`, `GET` with the same as query parameters (queries only:
  a mutation over `GET` gets 405), and batches (a JSON array of requests, `graphql.max_batch`).
  Clients that accept `application/graphql-response+json` get 400 for requests that fail before
  execution; `application/json` clients get 200 for every well-formed request, as the
  specification asks.
- **Validation.** Requests are checked against the schema with the rules of graphql-js and its
  messages (unknown fields and arguments, wrong values and variable types, fragments that can't
  apply, fields that can't be merged...). Errors have `locations` and `extensions.code`
  (`GRAPHQL_PARSE_FAILED`, `GRAPHQL_VALIDATION_FAILED`, `BAD_USER_INPUT`), and the log shows each
  as a violation.
- **Execution.** Fragments, inline fragments, aliases, variables with defaults, `@skip`,
  `@include` and `__typename` behave as in a real server. Interfaces and unions return one of
  their object types. A field that fails is null and its error has the field's `path`; a
  non-null field's null goes up to the nearest nullable one.
- **Introspection.** `__schema` and `__type` are complete, so GraphiQL, Apollo Sandbox and
  code generators work against the mock (`graphql.introspection: false` turns it off). The SDL is
  at `/graphql/schema.graphql` and `GET /__apigenie/spec`.
- **GraphiQL.** A browser opening the endpoint gets GraphiQL, loaded from esm.sh
  (`graphql.playground: false` turns it off).

## Generated data

Values follow the field's type: enum values, list lengths between `graphql.lists.min` and
`max` (or a `first`, `last`, `limit`, `count`... argument, the field's own or its
connection's), nulls with `graphql.null_probability`, and realistic values for fields named like
`email`, `city`, `phone`, `url` or `firstName`, for `name`, `title` and `genre` by the type
they belong to (`Author.name` is a person's, `Book.title` a book's), and for custom scalars by
their name (`DateTime`, `Date`, `URL`, `UUID`, `JSON`, `Long`, `Decimal`, `EmailAddress`...).
`--seed` repeats a run.

The schema can say more, with graphql-faker's directives or the mock's own. They need no
definitions in the schema and don't show in introspection:

```graphql
type User {
  name: String @fake(type: fullName)
  email: String @fake(type: email, options: {emailProvider: "example.com"})
  joined: String @fake(type: pastDate, options: {dateFormat: "YYYY-MM-DD"})
  role: Role @examples(values: ["ADMIN", "EDITOR"])
  tags: [String!] @listLength(min: 1, max: 3)
  score: Int @mock(min: 0, max: 100)
  code: String @mock(faker: "string.numeric", args: [6])
  nickname: String @mock(nullProbability: 0.5)
}
scalar Money @mock(min: 1, max: 500)
```

- `@fake(type:, options:)` takes the 64 types of graphql-faker and the options most used
  (`minMoney`, `maxMoney`, `decimalPlaces`, `minNumber`, `maxNumber`, `precisionNumber`,
  `dateFormat`, `dateFrom`, `dateTo`, `loremSize`, `imageSize`, `imageKeywords`,
  `passwordLength`, `emailProvider`, `useFullAddress`). Data is English whatever the `locale`.
- `@examples(values:)` picks one of the values; `@listLength(min:, max:)` sizes a list.
- `@mock` takes `faker` (any faker provider, as `x-faker`) with `args` (a list by position or an
  object by name), `value` or `values`,
  `min` and `max` for numbers, `length` or `minLength` and `maxLength` for lists, and
  `nullProbability`. On a root field, `delay` (milliseconds) delays the response and
  `errorRate` with `error` makes it fail; on a subscription field, `interval` and `count` pace its
  events.

`graphql.fields` does the same from the configuration, for a field (`Type.field`) or a scalar,
over the schema's directives. A schema that defines a directive of the same name keeps it: the
mock leaves it alone.

## The store

Objects whose type has an `id` field are entities: once made, their fields keep their values
for the next requests, so `{ user(id: "7") { name } }` answers the same name every time. The
usual root fields work on them, by their names and types:

| Field | What it does |
| --- | --- |
| `user(id: ID!): User`, `node(id: ID!): Node` | The entity with that id; a new one when there is none (`graphql.store.unknown_ids: null` answers null). |
| `users: [User!]!` | The users, made up to the list's length once, then the same. |
| `usersConnection(first:, after:, last:, before:)` | The users as a Relay connection: `edges { cursor node }`, `nodes`, `pageInfo`, `totalCount`. |
| `createUser(input: ...)`, `addUser(...)`, `newUser`, `registerUser`... | A new user with the input's fields; payload types get it in their field and `clientMutationId` back. |
| `updateUser(id:, input:)`, `editUser`, `setUser`... | The user, with the input's fields. |
| `deleteUser(id:)`, `removeUser`... | Removes the user; returns it, `true`, its id or a payload. |

`--dynamic` and `graphql.store.enabled: false` turn the store off; `POST /__apigenie/seed`
empties it, and so does a reload. `graphql.store.max_entities` (10000) bounds it.

## Examples

An example answers a request with data of its own. Operation examples match an operation's
name, its document (layout and comments don't count) and some of its variables; field examples
are the value of a field for some arguments, in any operation:

```yaml
# schema_examples/users.yaml, next to schema.graphql: an example, or a list of them
- name: ada
  operation: GetUser
  variables: {id: "1"}
  response:
    data: {user: {id: "1", name: Ada Lovelace}}
- name: grace            # a field example
  field: Query.user
  arguments: {id: "2"}
  data: {name: Grace Hopper}
```

Example data is cut to the request's selection set, and fields it lacks are generated, so one
example serves every query of its operation. Examples come from `graphql.examples` in the
configuration, from `graphql.examples_files` (files and directories of `.json`, `.yaml` and
`.yml`, each an example or a list of them) and from the directory named after the schema
(`schema.graphql` takes `schema_examples/`). `Prefer: example=ada` picks one by name;
`--dynamic` and `Prefer: dynamic=true` leave them out. An example's `errors` and `extensions`
are added to the response, and `status` sets the HTTP status.

## Scenarios and errors

Rules of scenarios and `operations` match GraphQL operations by the operation's name
(`operationId: GetUser`) or by a root field (`operationId: Query.user`, or `tag: Query.user`).
Besides `status`, `headers`, `body` (the whole response), `example`, `dynamic` and the
simulation, they take `errors`:

```yaml
scenarios:
  users-down:
    rules:
      - match: {operationId: Query.user}
        errors:
          - message: The users service is down.
            path: [user]
            extensions: {code: UNAVAILABLE}
          - Partial results      # no path: added to the response's errors
```

An error with a path fails that field: it is null (or null up to the nearest nullable field)
and the error has its locations. `Prefer: code=`, `example=`, `dynamic=`, `delay=`, `drop=` and
`scenario=` apply as for OpenAPI.

## Subscriptions

WebSocket clients of the endpoint speak `graphql-transport-ws` (the `graphql-ws` library: Apollo
Client, urql, GraphiQL) or the legacy `graphql-ws` of `subscriptions-transport-ws`, by the
subprotocol they offer. A subscription gets an event every `graphql.subscriptions.interval`
(1000 ms), each a fresh execution of its operation, until `count` events (0: until the client
completes it); a field's `@mock(interval:, count:)` wins. Queries and mutations over WebSocket get
one result. A request that accepts `text/event-stream` gets the same as server-sent events
(`next` events, then `complete`: graphql-sse's distinct connections mode). The log has `ws-open`,
`ws-in`, `ws-out` and `ws-close` events; the server's stop closes the clients with 1001 and a
reload with 1012.

## Contract testing of a GraphQL API

With `--proxy-to`, the GraphQL endpoint goes to a real GraphQL server, and the mock checks every
request against the schema and every answer against the schema and the request's selection set.
Point your frontend, your tests or a load test at the mock, and the traffic becomes a contract
test of the server:

```sh
apigenie-server schema.graphql --proxy-to http://localhost:4000/graphql --report contract.json --fail-on errors
```

```text
Listening on http://127.0.0.1:4010/graphql (spec: schema.graphql, 12 operations)
Proxy: http://localhost:4000/graphql (requests: log, responses: log, unknown routes: forward, mode: always)
```

The URL's path is the server's endpoint (`HOST:PORT` alone keeps the mock's, `/graphql`). The
upstream gets the request as it came (a batch stays a batch), without the mock's `__` query
parameters and `X-Apigenie-Mock-*` headers, with `Via` and `X-Forwarded-*`, and
`Accept-Encoding: identity`. GraphiQL, `/graphql/schema.graphql`, the admin API and `OPTIONS`
stay local. Requests for other paths are forwarded unchecked (`request.path.unknown`), or answered
404 with `--proxy-unknown reject`. A proxy makes up no subscription events: WebSocket and
server-sent events subscriptions aren't served.

**What is checked.** Requests as the mock checks them (graphql-js's rules and messages), and each
answer as graphql-js would have produced it, field by field:

| Check | Severity | When |
| --- | --- | --- |
| `request.body.malformed` | error | Not a GraphQL request: not JSON, no query, an unsupported content type. |
| `request.query.malformed`, `request.operation.unknown` | error | The query can't be parsed; `operationName` names no operation, or is missing. |
| `request.query.invalid`, `request.variables.incompatible` | error | The operation isn't valid against the schema; a variable doesn't match its type. |
| `request.method.incompatible` | error | A mutation over `GET`. |
| `request.field.deprecated` | info | A deprecated field, argument, input field or enum value is used. |
| `request.path.unknown` | warn | A request for another path than the endpoint (forwarded unchecked). |
| `response.status.incompatible` | error | A 1xx or 3xx status, or a 4xx with `data`: an executed operation that says it failed. |
| `response.status.server-error` | info | The server answered with 5xx. |
| `response.content-type.incompatible` | error | Neither `application/json` nor `application/graphql-response+json`. |
| `response.body.malformed` | error | Not JSON, or a batch not answered with a list of as many results. |
| `response.shape.incompatible` | error | Not a map of `data`, `errors` and `extensions`: other keys, neither `data` nor `errors`, `data: null` without `errors`, empty `errors`. |
| `response.errors.incompatible` | error | An error without a string `message`, with `locations` that aren't `{line, column}`, a `path` of other than names and indexes, `extensions` that aren't an object. |
| `response.errors.unexpected`, `response.errors.path-unknown` | warn | An error's entries outside `extensions`; a `path` that names no field the operation selects. |
| `response.data.incompatible` | error | A selected field (or alias) missing; `null` in a non-null position that no error's `path` explains; a list, an object or a scalar where the type says otherwise; `Int` beyond 32 bits, a non-finite `Float`, a `String`, `Boolean` or `ID` of another kind, an unknown enum value; a `__typename` that isn't a possible type. |
| `response.data.unexpected` | warn | A key the operation didn't select. |
| `response.data.typename-missing` | info | An object of an interface or union came without `__typename`: only the selections outside type conditions are checked. |
| `response.data.unvalidatable` | info | Nesting deeper than 64, or more than 100 violations in one answer. |
| `response.field-error` | warn | The server ran the operation, but fields failed: `errors` next to `data` (`null` or partial), one per error, e.g. `the field book failed: not implemented`. Valid GraphQL, but a contract test shouldn't pass it silently; `--fail-on warnings` (or `contract.severity`) fails on it. |
| `response.request-error` | warn | The server didn't execute the operation (authentication, persisted queries...). |
| `response.schema-drift` | error | The server rejected an operation the schema allows (`GRAPHQL_VALIDATION_FAILED`, "Cannot query field..."): its schema differs. |
| `schema.drift`, `schema.addition` | error, info | The server's schema (introspected) lacks or changed something of the schema; has more. |
| `schema.unchecked` | info | The server's schema can't be introspected. |

Fragments, inline fragments, `@skip` and `@include` (with the request's variables), aliases and
merged fields are followed as the server must; custom scalars take any JSON value.
`contract.severity` changes a check's severity or turns it off, as for OpenAPI.

**The server's schema.** With the first request it forwards, the proxy also sends the server the
usual introspection query (with that request's `Authorization`, `Cookie` and `X-Api-Key`), turns
the answer into a schema and compares it with the served one, as `apigenie diff` does: what the
server lacks or changed incompatibly is `schema.drift` (located by `Type.field`), what it adds
`schema.addition`. The log has a `server` event with the result (`schema of
http://localhost:4000/graphql: 1 breaking difference, 2 additions`), and the report a `schema`
section. A server that isn't up yet is asked again with the next request; `POST
/__apigenie/contract/schema-check` asks now. `proxy.schema_check: false` turns it off.

**Report or enforce.** As for OpenAPI, the proxy reports by default: answers carry
`X-Apigenie-Mock-Violations` and `X-Apigenie-Mock-Violations-Detail` (locations such as
`["response", "body", "/data/book/title"]`, `/2/data/...` for the third result of a batch). With
`--strict`, a request with errors isn't forwarded: it gets the GraphQL errors the mock answers with
(`GRAPHQL_VALIDATION_FAILED`...). An answer with errors is replaced by the `invalid-upstream-response`
502 problem. Warnings and notes never block.

**Fallback mode.** `--proxy-mode fallback` (`proxy.mode`) lets scenario rules, examples and the
scripts' `onGraphQL` answer first, as for a mock; what nothing answers goes to the server. A batch
goes to the server whole as soon as one of its operations does.

**Recording.** `--record-dir DIR` writes the answer to each valid operation as an example file
(`GetBook-1.yaml`: `name`, `operation`, `query`, `variables`, `response`, and `status` when not
200), which the mock serves as it is: point `graphql.examples_files` at the directory, or name it
after the schema (`schema_examples/`). Bodies are recorded as they are.

**Coverage and reports.** The mock counts the schema's fields that requests selected (every field
of its object and interface types, as GraphQL Inspector's coverage does) and its root fields
(`Query.book`): `covered`, `partial` (some fields under it never selected) or `not-tested`. Mocked
traffic counts too. `GET /__apigenie/contract` is the report; when the server stops it prints a
summary, writes `--report` and `--report-junit` (a test case per root field, one for the server's
schema, one for the coverage threshold) and exits with 5 when `--fail-on` or `--min-coverage`
(the share of fields) isn't met. The JSON report has `summary`, `schema`, `rootFields`, `fields`,
`uncoveredFields`, `operations` (by name), `otherRoutes` and `violations`.
