Skip to content

Latest commit

 

History

History
150 lines (104 loc) · 8.19 KB

File metadata and controls

150 lines (104 loc) · 8.19 KB

Using GraphQL from Stripes

Introduction

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:

However, so far we have been using react-apollo, the React integration of Apollo, so that's what we'll describe here.

Using Apollo from a Stripes module

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-tag and react-apollo to your module's package file. (Re-run yarn install to get them.)
  • Modify the component that will use a GraphQL query:
    1. Import { graphql } from 'react-apollo' and gql from 'graphql-tag';
    2. Define a constant, QUERY say, containing the compiled query. This is most easy done using the gql tag.
    3. Add data: PropTypes.shape({ ... }) to your component's property-types.
    4. Instead of export default MyComponent, wrap this as export default graphql(QUERY)(MyComponent).
    5. Modify your render method to use the props.data returned from the GraphQL service.
  • If your component was previously using stripes-connect, rip out the manifest, the connect wrapping and the references to props.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.

Incorporating variables

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,
  },
}),

Making and injecting CQL queries

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.)

Invoking <SearchAndSort>

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 the totalRecords part that contains the count of matching records.
  • queryFunction={QueryFunction} -- the function generated by makeQueryFunction, 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.)

Worked example

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.)

Adding support for new complex elements

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.)