Skip to content

Commit 600a350

Browse files
ddebrunnerbobbiejc
andauthored
Persisted documents snippet. (#54)
* persisted document snippet * chore: cleanup * chore: cleanup * fix spelling Co-authored-by: Bobbie Cochrane <[email protected]> --------- Co-authored-by: Bobbie Cochrane <[email protected]>
1 parent 8e7e8d2 commit 600a350

File tree

6 files changed

+277
-10
lines changed

6 files changed

+277
-10
lines changed

executable/persisted/README.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Persisted Documents
2+
3+
GraphQL _executable documents_ containing at least one operation definition
4+
and optional fragment defintions can be persisted with a schema.
5+
6+
## Overview
7+
8+
The optional `executables` argument of `@sdl` takes a list of input document files
9+
that are GraphQL _executable documents_.
10+
11+
```
12+
@sdl(
13+
files: []
14+
executables: [{ document: "operations.graphql", persist: true }]
15+
)
16+
```
17+
18+
When a schema is deployed the _executable documents_ must validate successfully
19+
against the schema.
20+
21+
By itself this can be used to validate applications' use
22+
of a GraphQL endpoint remain valid when the schema changes.
23+
This requires that the application loads GraphQL requests from
24+
files containing executable documents that are declared in the schema.
25+
26+
In addition any _executable document_ in the schema that is marked with `persist: true`
27+
is loaded as a _persisted document_.
28+
29+
A client uses a _persisted document_ by specifying its document identifier
30+
(based upon a SHA256 hash) in a request instead of the content of the _executable document_.
31+
32+
For example instead of this POST body for a request:
33+
34+
```
35+
{
36+
"query": "{__typname}"
37+
}
38+
```
39+
40+
a request can use this:
41+
42+
```
43+
{
44+
"documentId": "sha256:ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"
45+
}
46+
```
47+
48+
if an _executable document_ was declared in the schema with the matching SHA256 hash.
49+
50+
The request parameters `operationName` and `variables` can be used as required with `documentId`.
51+
52+
## Benefits
53+
54+
Use of persisted documents typically saves network bandwidth as for real application requests
55+
the hash is smaller than the body of the _executable document_.
56+
57+
In addition query requests can use HTTP GET while maintaining a reasonable sized URL
58+
that improves caching of URL requests. For example the above request can be a coded as an HTTP GET:
59+
60+
```
61+
https://london.us-east-a.ibm.stepzen.net/api/customer/graphql?documentId=sha256%3Aecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38
62+
```
63+
64+
## Executable Documents
65+
66+
A good practice is to ensure that a CI/CD process ensures that _executable documents_ declared as
67+
part of the schema using `@sdl(executables:)` are formatted consistently, using tooling such as `prettier`.
68+
69+
For example the simple _executable document_ shown above `{__typename}` stored in a document file
70+
and formatted will have contents (including a newline at the end):
71+
72+
```
73+
{
74+
__typename
75+
}
76+
```
77+
78+
and thus a document identifier of `sha256:8d8f7365e9e86fa8e3313fcaf2131b801eafe9549de22373089cf27511858b39`.
79+
80+
Clients can obtain the SHA256 hash for a document identifer using any standard mechanism for calculating hashes,
81+
for example on Linux/Unix systems this command can be used:
82+
83+
```
84+
shasum -a 256 operations.graphql
85+
```
86+
87+
When _executable documents_ are persisted using `@sdl(executables:)` the schema calculates the document identifiers automatically.
88+
89+
## Example
90+
91+
This example uses a simple mocked schema with a `Customer` type and a single `Query` field `customer`.
92+
93+
`operations.graphql` contains three GraphQL query operations `Customer`, `CustomerEmail` and `CustomerName`
94+
each with a different selection against `Query.customer`.
95+
96+
A client can execute them using these request parameters, shown as JavaScript:
97+
98+
```
99+
{
100+
documentId: "sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
101+
operationName: "Customer",
102+
variables: {
103+
id: 1789,
104+
},
105+
}
106+
```
107+
108+
```
109+
{
110+
documentId: "sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
111+
operationName: "CustomerEmail",
112+
variables: {
113+
id: 2845,
114+
},
115+
}
116+
```
117+
118+
```
119+
{
120+
documentId: "sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
121+
operationName: "CustomerName",
122+
variables: {
123+
id: 3651,
124+
},
125+
}
126+
```
127+
128+
For example use `curl` and HTTP `GET` as follows:
129+
130+
```
131+
curl \
132+
--header "Authorization: Apikey $(stepzen whoami --apikey)" \
133+
--header "Content-Type: application/json" \
134+
'https://london.us-east-a.ibm.stepzen.net/api/miscellaneous/graphql?documentId=sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0&operationName=Customer&variables=%7B%22id%22%3A%201789%7D'
135+
```

executable/persisted/index.graphql

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
schema
2+
@sdl(
3+
files: []
4+
# executables defines a list of GraphQL executable documents that
5+
# are validated against the schema when deployed.
6+
# If an executable document is marked as persist: true
7+
# then it becomes a persisted document with the document identifier
8+
# being tha SHA 256 hash of the document file.
9+
executables: [{ document: "operations.graphql", persist: true }]
10+
) {
11+
query: Query
12+
}
13+
14+
type Query {
15+
customer(id: ID!): Customer
16+
}
17+
type Customer @mock {
18+
id: ID!
19+
name: String! @mockfn(name: "LastName")
20+
email: String @mockfn(name: "Email")
21+
phone: String @mockfn(name: "Phone")
22+
address: Address
23+
}
24+
type Address {
25+
city: String @mockfn(name: "City")
26+
zip: String @mockfn(name: "Zip")
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
query Customer($id: ID!) {
2+
customer(id: $id) {
3+
id
4+
name
5+
email
6+
phone
7+
address {
8+
city
9+
zip
10+
}
11+
}
12+
}
13+
14+
query CustomerName($id: ID!) {
15+
customer(id: $id) {
16+
name
17+
}
18+
}
19+
20+
query CustomerEmail($id: ID!) {
21+
customer(id: $id) {
22+
email
23+
}
24+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"endpoint": "api/miscellaneous"
3+
}

executable/persisted/tests/Test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const fs = require("fs");
2+
const path = require("node:path");
3+
4+
const {
5+
deployAndRun,
6+
authTypes,
7+
getTestDescription,
8+
} = require("../../../tests/gqltest.js");
9+
10+
testDescription = getTestDescription("snippets", __dirname);
11+
12+
const requestsFile = path.join(path.dirname(__dirname), "operations.graphql");
13+
const requests = fs.readFileSync(requestsFile, "utf8").toString();
14+
15+
describe(testDescription, function () {
16+
const tests = [
17+
{
18+
label: "CustomerName",
19+
documentId:
20+
"sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
21+
operationName: "CustomerName",
22+
variables: {
23+
id: 1031,
24+
},
25+
expected: {
26+
customer: {
27+
name: "Tromp",
28+
},
29+
},
30+
authType: authTypes.adminKey,
31+
},
32+
{
33+
label: "CustomerName",
34+
documentId:
35+
"sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
36+
operationName: "CustomerEmail",
37+
variables: {
38+
id: 2845,
39+
},
40+
expected: {
41+
customer: {
42+
43+
},
44+
},
45+
authType: authTypes.adminKey,
46+
},
47+
{
48+
label: "Customer",
49+
documentId:
50+
"sha256:9d50d8e35b5882139e836a126f5d6d5a28cf41c5efd80a6e67f920d284b5f6d0",
51+
operationName: "Customer",
52+
variables: {
53+
id: 3293,
54+
},
55+
expected: {
56+
customer: {
57+
id: "3293",
58+
name: "Veum",
59+
email: null,
60+
phone: "5349179326",
61+
address: {
62+
city: "New Abshire",
63+
zip: "75624",
64+
},
65+
},
66+
},
67+
authType: authTypes.adminKey,
68+
},
69+
];
70+
return deployAndRun(__dirname, tests);
71+
});

tests/gqltest.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function deployEndpoint(endpoint, dirname) {
4747
// as a test returning the response.
4848
// The test will fail if the request does not
4949
// have status 200 or has any GraphQL errors.
50-
async function runGqlOk(authType, endpoint, query, variables, operationName, expected) {
50+
async function runGqlOk(authType, endpoint, request, expected) {
5151
let headers = new GQLHeaders();
5252
switch (authType) {
5353
case authTypes.adminKey:
@@ -65,11 +65,7 @@ async function runGqlOk(authType, endpoint, query, variables, operationName, exp
6565
test: this,
6666
endpoint,
6767
headers,
68-
request: {
69-
query: query,
70-
variables: variables,
71-
operationName: operationName,
72-
},
68+
request,
7369
expected,
7470
})
7571
}
@@ -87,15 +83,26 @@ function deployAndRun(dirname, tests) {
8783

8884
afterEach('log-failure', logOnFail)
8985
tests.forEach(
90-
({ label, query, variables, operationName, expected, authType }) => {
86+
({ label, documentId, query, variables, operationName, expected, authType }) => {
9187
it(label, async function () {
9288
this.timeout(4000); // Occasional requests take > 2s
89+
let request = {}
90+
if (query) {
91+
request.query = query;
92+
}
93+
if (documentId) {
94+
request.documentId = documentId;
95+
}
96+
if (operationName) {
97+
request.operationName = operationName;
98+
}
99+
if (variables) {
100+
request.variables = variables;
101+
}
93102
return await runGqlOk(
94103
authType,
95104
endpoint,
96-
query,
97-
variables,
98-
operationName,
105+
request,
99106
expected,
100107
);
101108
});

0 commit comments

Comments
 (0)