Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions executable/persisted/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Persisted Documents

GraphQL _executable documents_ containing at least one operation definition
and optional fragment defintions can be persisted with a schema.

## Overview

The optional `executables` argument of `@sdl` takes a list of input document files
that are GraphQL _executable documents_.

```
@sdl(
files: []
executables: [{ document: "operations.graphql", persist: true }]
)
```

When a schema is deployed the _executable documents_ must validate successfully
against the schema.

By itself this can be used to validate applications' use
of a GraphQL endpoint remain valid when the schema changes.
This requires that the application loads GraphQL requests from
files containing executable documents that are declared in the schema.

In addition any _executable document_ in the schema that is marked with `persist: true`
is loaded as a _persisted document_.

A client uses a _persisted document_ by specifying its document identifier
(based upon a SHA256 hash) in a request instead of the content of the _executable document_.

For example instead of this POST body for a request:

```
{
"query": "{__typname}"
}
```

a request can use this:

```
{
"documentId": "sha256:ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"
}
```

if an _executable document_ was declared in the schema with the matching SHA256 hash.

The request parameters `operationName` and `variables` can be used as required with `documentId`.

## Benefits

Use of persisted documents typically saves network bandwidth as for real application requests
the hash is smaller than the body of the _executable document_.

In addition query requests can use HTTP GET while maintaining a reasonable sized URL
that improves caching of URL requests. For example the above request can be a coded as an HTTP GET:

```
https://london.us-east-a.ibm.stepzen.net/api/customer/graphql?documentId=sha256%3Aecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38
```

## Executable Documents

A good practice is to ensure that a CI/CD process ensures that _executable documents_ declared as
part of the schema using `@sdl(executables:)` are formatted consistently, using tooling such as `prettier`.

For example the simple _executable document_ shown above `{__typename}` stored in a document file
and formatted will have contents (including a newline at the end):

```
{
__typename
}
```

and thus a document identifier of `sha256:8d8f7365e9e86fa8e3313fcaf2131b801eafe9549de22373089cf27511858b39`.

Clients can obtain the SHA256 hash for a document identifer using any standard mechanism for calculating hashes,
for example on Linux/Unix systems this command can be used:

```
shasum -a 256 operations.graphql
```

When _executable documents_ are persisted using `@sdl(executables:)` the schema calculates the document identifiers automatically.

## Example

This example uses a simple mocked schema with a `Customer` type and a single `Query` field `customer`.

`operations.graphql` contains three GraphQL query operations `Customer`, `CustomerEmail` and `CustomerName`
each with a different selection against `Query.customer`.

A client can execute them using these request parameters, shown as JavaScript:

```
{
documentId: "sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
operationName: "Customer",
variables: {
id: 1789,
},
}
```

```
{
documentId: "sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
operationName: "CustomerEmail",
variables: {
id: 2845,
},
}
```

```
{
documentId: "sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
operationName: "CustomerName",
variables: {
id: 3651,
},
}
```

For example use `curl` and HTTP `GET` as follows:

```
curl \
--header "Authorization: Apikey $(stepzen whoami --apikey)" \
--header "Content-Type: application/json" \
'https://london.us-east-a.ibm.stepzen.net/api/miscellaneous/graphql?documentId=sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0&operationName=Customer&variables=%7B%22id%22%3A%201789%7D'
```
27 changes: 27 additions & 0 deletions executable/persisted/index.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
schema
@sdl(
files: []
# executables defines a list of GraphQL executable documents that
# are validated against the schema when deployed.
# If an executable document is marked as persist: true
# then it becomes a persisted document with the document identifier
# being tha SHA 256 hash of the document file.
executables: [{ document: "operations.graphql", persist: true }]
) {
query: Query
}

type Query {
customer(id: ID!): Customer
}
type Customer @mock {
id: ID!
name: String! @mockfn(name: "LastName")
email: String @mockfn(name: "Email")
phone: String @mockfn(name: "Phone")
address: Address
}
type Address {
city: String @mockfn(name: "City")
zip: String @mockfn(name: "Zip")
}
24 changes: 24 additions & 0 deletions executable/persisted/operations.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
query Customer($id: ID!) {
customer(id: $id) {
id
name
email
phone
address {
city
zip
}
}
}

query CustomerName($id: ID!) {
customer(id: $id) {
name
}
}

query CustomerEmail($id: ID!) {
customer(id: $id) {
email
}
}
3 changes: 3 additions & 0 deletions executable/persisted/stepzen.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"endpoint": "api/miscellaneous"
}
71 changes: 71 additions & 0 deletions executable/persisted/tests/Test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const fs = require("fs");
const path = require("node:path");

const {
deployAndRun,
authTypes,
getTestDescription,
} = require("../../../tests/gqltest.js");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to define the REPO_ROOT like we have done in our Makefiles? this ../../../.. nonsense always catches me. somehow I could do this once at the top of the file more easily -- I guess in this case it's only used once...

testDescription = getTestDescription("snippets", __dirname);

const requestsFile = path.join(path.dirname(__dirname), "operations.graphql");
const requests = fs.readFileSync(requestsFile, "utf8").toString();

describe(testDescription, function () {
const tests = [
{
label: "CustomerName",
documentId:
"sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
operationName: "CustomerName",
variables: {
id: 1031,
},
expected: {
customer: {
name: "Tromp",
},
},
authType: authTypes.adminKey,
},
{
label: "CustomerName",
documentId:
"sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
operationName: "CustomerEmail",
variables: {
id: 2845,
},
expected: {
customer: {
email: "[email protected]",
},
},
authType: authTypes.adminKey,
},
{
label: "Customer",
documentId:
"sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
operationName: "Customer",
variables: {
id: 3293,
},
expected: {
customer: {
id: "3293",
name: "Veum",
email: null,
phone: "5349179326",
address: {
city: "New Abshire",
zip: "75624",
},
},
},
authType: authTypes.adminKey,
},
];
return deployAndRun(__dirname, tests);
});
27 changes: 17 additions & 10 deletions tests/gqltest.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function deployEndpoint(endpoint, dirname) {
// as a test returning the response.
// The test will fail if the request does not
// have status 200 or has any GraphQL errors.
async function runGqlOk(authType, endpoint, query, variables, operationName, expected) {
async function runGqlOk(authType, endpoint, request, expected) {
let headers = new GQLHeaders();
switch (authType) {
case authTypes.adminKey:
Expand All @@ -65,11 +65,7 @@ async function runGqlOk(authType, endpoint, query, variables, operationName, exp
test: this,
endpoint,
headers,
request: {
query: query,
variables: variables,
operationName: operationName,
},
request,
expected,
})
}
Expand All @@ -87,15 +83,26 @@ function deployAndRun(dirname, tests) {

afterEach('log-failure', logOnFail)
tests.forEach(
({ label, query, variables, operationName, expected, authType }) => {
({ label, documentId, query, variables, operationName, expected, authType }) => {
it(label, async function () {
this.timeout(4000); // Occasional requests take > 2s
let request = {}
if (query) {
request.query = query;
}
if (documentId) {
request.documentId = documentId;
}
if (operationName) {
request.operationName = operationName;
}
if (variables) {
request.variables = variables;
}
return await runGqlOk(
authType,
endpoint,
query,
variables,
operationName,
request,
expected,
);
});
Expand Down