- Introduction
- Using Apollo from a Stripes module
- Incorporating variables
- Making and injecting CQL queries
- Invoking
<SearchAndSort> - Worked example
- Adding support for new complex elements
FOLIO UI code is written in ES6, a modern dialect of JavaScript, using the Stripes framework. In order to access GraphQL services such as that provided by mod-graphql, Stripes modules can use whatever ES6-compatible method they wish, including:
- Direct HTTP POSTing using
fetch - Facebook's Relay library
- The low-level Apollo library
- Many other options
However, so far we have been using react-apollo, the React integration of Apollo, so that's what we'll describe here.
Just as your module uses stripes-connect by declaring a manifest, wrapping the component in a connect HOC and then accesses the resource prop provided by the data layer, so you use Apollo by wrapping the component in the graphql HOC and accessing the data prop. There is no direct analogue of the stripes-connect manifest. Instead, the GraphQL query is passed into the HOC; however, it is often convenient to define that query statically.
Here are the steps:
- Add
graphql-tagandreact-apolloto your module's package file. (Re-runyarn installto get them.) - Modify the component that will use a GraphQL query:
- Import
{ graphql } from 'react-apollo'andgql from 'graphql-tag'; - Define a constant,
QUERYsay, containing the compiled query. This is most easy done using thegqltag. - Add
data: PropTypes.shape({ ... })to your component's property-types. - Instead of
export default MyComponent, wrap this asexport default graphql(QUERY)(MyComponent). - Modify your
rendermethod to use theprops.datareturned from the GraphQL service.
- Import
- If your component was previously using stripes-connect, rip out the manifest, the
connectwrapping and the references toprops.resources.
In fact, idiosyncrasies of react-apollo require a slightly more complex wrapping than implied in step 4 above:
export default graphql(QUERY, {
options: props => ({
notifyOnNetworkStatusChange: true,
errorPolicy: 'all',
}),
})(MyComponent);
These options are necessary to get the client library to respond helpfully to various classes of error.
If your main GraphQL query incorporates variables -- as for example
const QUERY = gql`
query ($id:String!) {
instance_storage_instances_SINGLE(instanceId: $id) {
id title
}
}
`;
Then you need to pass those variables into the graphql HOC. This is done by providing a variables element to the options object shown above. Its value is an object mapping variable names to their values: for example, if the ID of the instance-record to fetch is in the {id} component of the route, then the graphql wrapper might use:
options: props => ({
notifyOnNetworkStatusChange: true,
errorPolicy: 'all',
variables: {
id: props.match.params.id,
},
}),
Things gets a little more complicated when injecting a CQL query in a GraphQL query -- as for example
query ($cql:String) {
instance_storage_instances(query: $cql) {
totalRecords
instances {
id title
}
}
}
In this case, the variables object that's placed in the graphql wrapper's options must contain a key cql whose value is generated dynamically. Typically, then, the object is generated by a function called something like makeVariables. If the props are passed into this, it can generate a CQL query from them in whatever way seems best to it.
Typically, of course, the best way to generate the CQL is using the stripes-smart-components utility function makeQueryFunction. This is a static function, called once with static configuration parameters, to generate a function that is called every time a query needs to be generated. Typically, a constant function will be stored with a name like QueryFunction when the component code is loaded; then this is invoked from makeVariables as needed:
const QueryFunction = makeQueryFunction(
'cql.allRecords=1',
'title="%{query.query}*" or author="%{query.query}*"',
{
Title: 'title',
Author: 'author',
publishers: 'publication',
},
filterConfig,
);
And then:
function makeVariables(props) {
const parsedQuery = queryString.parse(props.location.search || '');
return {
cql: QueryFunction(parsedQuery, props, props.resources, props.stripes.logger),
};
}
export default graphql(QUERY, {
options: props => ({
notifyOnNetworkStatusChange: true,
errorPolicy: 'all',
variables: makeVariables(props)
}),
})(MyComponent);
(This could be done a little more compactly by including the body of makeVariables as a closure within the graphql wrapping, but as we shall see, we need the constant QueryFunction again if we're going to use Stripes' searching-and-sorting utilities.)
If your component offers searching and sorting functionality, using the <SearchAndSort> component, you will need to pass in several properties to that component:
parentData={this.props.data}-- provides the actual records or diagnostics returned from the GraphQL server.apolloResource="instance_storage_instances"-- the name of the GraphQL entry-point that gets run.apolloRecordsKey="instances"-- the name of the part of the data object that contains the records to display, as opposed to thetotalRecordspart that contains the count of matching records.queryFunction={QueryFunction}-- the function generated bymakeQueryFunction, which is used by the paging code to create the query used for next-page requests.apolloQuery={QUERY}-- (actually, it's not clear that this gets used at all.)
The changes made to ui-inventory's top-level instance search illustrate the procedures described above. (Switch to the "Files changed" tab.)
(Note that the stripes-connect manifest that gets removed in this change is unusually enormous: that's because it contains a non-DRY variant of the makeQueryFunction code.)
When adding support for an additional complex element in an existing type -- a new nested layer -- you do not need to make any changes on the server side, as the existing RAMLs and JSON Schemas already describe the capability of the underlying back-end modules. But you do need to modify the client code.
Here, we use the example of adding items within the holdings records associated with instances, which required a change to ui-inventory.
In the GraphQL query sent by the client -- typically declared using a gql tag -- add the new element including all the desired subelements. In the present case, this means adding holdingsItems within the request for holdingsRecords.
Including the sub-elements is easy to overlook -- and very difficult to diagnose, because in this case mod-graphql simply does not return the newly added elements, and issues no diagnostics. It's quite right to behave this way: it's doing exactly what the client code is requesting. (If you think you detect here the kind of wisdom that comes only by experience, you are quite correct.)