JavaScript support for GraphQL projects using graphql-pro’s OperationStore
for persisted queries.
sync
CLISee the OperationStore guide for server-side setup.
sync
utilityThis package contains a command line utility, graphql-ruby-client sync
:
$ graphql-ruby-client sync # ...
Authorizing with HMAC
Syncing 4 operations to http://myapp.com/graphql/operations...
3 added
1 not modified
0 failed
Generating client module in app/javascript/graphql/OperationStoreClient.js...
✓ Done!
sync
Takes several options:
option | description |
---|---|
--url |
Sync API url |
--path |
Local directory to search for .graphql / .graphql.js files |
--relay-persisted-output |
Path to a .json file from relay-compiler ... --persist-output |
--apollo-codegen-json-output |
Path to a .json file from apollo client:codegen ... --target json |
--apollo-android-operation-output |
Path to an OperationOutput.json file from Apollo Android |
--client |
Client ID (created on server) |
--secret |
Client Secret (created on server) |
--outfile |
Destination for generated code |
--outfile-type |
What kind of code to generate (js or json ) |
--header={key}:{value} |
Add a header to the outgoing HTTP request (may be repeated) |
--add-typename |
Add __typename to all selection sets (for use with Apollo Client) |
--verbose |
Output some debug information |
--changeset-version |
Set a Changeset Version when syncing these queries. (context[:changeset_version] will also be required at runtime, when running these stored operations.) |
You can see these and a few others with graphql-ruby-client sync --help
.
graphql-ruby-client
can persist queries from relay-compiler
using the embedded @relayHash
value. (This was created in Relay before 2.0.0. See below for Relay 2.0+.)
To sync your queries with the server, use the --path
option to point to your __generated__
directory, for example:
# sync a Relay project
$ graphql-ruby-client sync --path=src/__generated__ --outfile=src/OperationStoreClient.js --url=...
Then, the generated code may be integrated with Relay’s Network Layer:
// ...
// require the generated module:
const OperationStoreClient = require('./OperationStoreClient')
// ...
function fetchQuery(operation, variables, cacheConfig, uploadables) {
const requestParams = {
variables,
operationName: operation.name,
}
if (process.env.NODE_ENV === "production")
// In production, use the stored operation
requestParams.operationId = OperationStoreClient.getOperationId(operation.name)
} else {
// In development, use the query text
requestParams.query = operation.text,
}
return fetch('/graphql', {
method: 'POST',
headers: { /*...*/ },
body: JSON.stringify(requestParams),
}).then(/* ... */);
}
// ...
(Only Relay Modern is supported. Legacy Relay can’t generate static queries.)
To use Relay’s persisted output, add a "file": ...
to your project’s persistConfig
object. For example:
"relay": {
...
"persistConfig": {
"file": "./persisted-queries.json"
}
},
Then, push Relay’s generated queries to your OperationStore server with --relay-persisted-output
:
$ graphql-ruby-client sync --relay-persisted-output=path/to/persisted-queries.json --url=...
In this case, sync
won’t generate a JavaScript module because relay-compiler
has already prepared its queries for persisted use. Instead, update your network layer to include the client name and operation id in the HTTP params:
const operationStoreClientName = "MyRelayApp";
function fetchQuery(operation, variables,) {
return fetch('/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
// Pass the client name and the operation ID, joined by `/`
documentId: operationStoreClientName + "/" + operation.id,
// query: operation.text, // this is now obsolete because text is null
variables,
}),
}).then(response => {
return response.json();
});
}
(Inspired by https://relay.dev/docs/guides/persisted-queries/#network-layer-changes.)
Now, your Relay app will only send operation IDs over the wire to the server.
Use the --path
option to point at your .graphql
files:
$ graphql-ruby-client sync --path=src/graphql/ --url=...
Then, load the generated module and add its .apolloMiddleware
to your network interface with .use([...])
:
// load the generated module
var OperationStoreClient = require("./OperationStoreClient")
// attach it as middleware in production
// (in development, send queries to the server as normal)
if (process.env.NODE_ENV === "production") {
MyNetworkInterface.use([OperationStoreClient.apolloMiddleware])
}
Now, the middleware will replace query strings with operationId
s.
Use the --path
option to point at your .graphql
files:
$ graphql-ruby-client sync --path=src/graphql/ --url=...
Then, load the generated module and add its .apolloLink
to your Apollo Link:
// load the generated module
var OperationStoreClient = require("./OperationStoreClient")
// Integrate the link to another link:
const link = ApolloLink.from([
authLink,
OperationStoreClient.apolloLink,
httpLink,
])
// Create a client
const client = new ApolloClient({
link: link,
cache: new InMemoryCache(),
});
Update the controller: Apollo Link supports extra parameters nested as params[:extensions][:operationId]
, so update your controller to add that param to context:
# app/controllers/graphql_controller.rb
context = {
# ...
# Support Apollo Link:
operation_id: params[:extensions][:operationId]
}
Now, context[:operation_id]
will be used to fetch a query from the database.
Use apollo client:codegen ... --target json
to build a JSON artifact containing your app’s queries. Then, pass the path to that artifact to graphql-ruby-client sync --apollo-codegen-json-output path/to/output.json ...
. sync
will use Apollo-generated operationId
s to populate the OperationStore
.
Then, to use Apollo-style persisted query IDs, hook up the Persisted Queries Link as described in Apollo’s documentation
Finally, update the controller to pass the Apollo-style persisted query ID as the operation ID:
# app/controllers/graphql_controller.rb
context = {
# ...
# Support already-synced Apollo Persisted Queries:
operation_id: params[:extensions][:operationId]
}
Now, Apollo-style persisted query IDs will be used to fetch operations from the server’s OperationStore
.
Apollo Android’s generateOperationOutput option builds an OperationOutput.json
file which works with the OperationStore. To sync those queries, use the --apollo-android-operation-output
option:
graphql-ruby-client sync --apollo-android-operation-output=path/to/OperationOutput.json --url=...
That way, the OperationStore will use the query IDs generated by Apollo Android.
On the server, you’ll have to update your controller to receive the client name and the operation ID. For example:
# app/controllers/graphql_controller.rb
context = { ... }
# Check for an incoming operation ID from Apollo Client:
apollo_android_operation_id = request.headers["X-APOLLO-OPERATION-ID"]
if apollo_android_operation_id.present?
# Check the incoming request to confirm that
# it's your first-party client with stored operations
client_name = # ...
if client_name.present?
# If we received an incoming operation ID
# _and_ identified the client, run a persisted operation.
context[:operation_id] = "#{client_name}/#{apollo_android_operation_id}"
end
end
You may also have to update your app to send an identifier, so that the server can determine the “client name” used with the operation store. (Apollo Android sends a query hash, but the operation store expects IDs in the form #{client_name}/#{query_hash}
.)
Apollo client has a Persisted Queries Link. You can use that link with GraphQL-Pro’s OperationStore. First, create a manifest with generate-persisted-query-manifest
, then, pass the path to that file to sync
:
$ graphql-ruby-client sync --apollo-persisted-query-manifest=path/to/manifest.json ...
Then, configure Apollo Client to use your persisted query manifest.
Finally, update your controller to receive the operation ID and pass it as context[:operation_id]
:
client_name = "..." # TODO: send the client name as a query param or header
persisted_query_hash = params[:extensions][:persistedQuery][:sha256Hash]
context = {
# ...
operation_id: "#{client_name}/#{persisted_query_hash}"
}
The operation_id
will also need your client name. Using Apollo Client, you could send this as a custom header or another way that works for your application (eg, session or user agent).
OperationStoreClient.getOperationId
takes an operation name as input and returns the server-side alias for that operation:
var OperationStoreClient = require("./OperationStoreClient")
OperationStoreClient.getOperationId("AppHomeQuery") // => "my-frontend-app/7a8078c7555e20744cb1ff5a62e44aa92c6e0f02554868a15b8a1cbf2e776b6f"
OperationStoreClient.getOperationId("ProductDetailQuery") // => "my-frontend-app/6726a3b816e99b9971a1d25a1205ca81ecadc6eb1d5dd3a71028c4b01cc254c1"
Post the operationId
in your GraphQL requests:
// Lookup the operation name:
var operationId = OperationStoreClient.getOperationId(operationName)
// Include it in the params:
$.post("/graphql", {
operationId: operationId,
variables: queryVariables,
}, function(response) {
// ...
})
OperationStore
uses HMAC-SHA256 to authenticate requests.
Pass the key to graphql-ruby-client sync
as --secret
to authenticate it:
$ export MY_SECRET_KEY= "abcdefg..."
$ graphql-ruby-client sync ... --secret=$MY_SECRET_KEY
# ...
Authenticating with HMAC
# ...