travis vachon

blasting off with solid

Over the past couple years the bulk of my professional work has taken the form of providing technical leadership for projects of the CoLab Cooperative, a worker-owned cooperative whose membership is distributed around the world. The work I've done there has informed my growing enthusiasm for Solid, the emerging web standard that promises to revolutionize how we build and publish on the World Wide Web. It has also given me a chance to build deep experience with modern React application architecture as I've lead the design and implementation of several React web and React Native applications for CoLab's clients. One of the most impressive libraries I've encountered in this time is Apollo Client, a "complete state management library for JavaScript apps" that makes building GraphQL-backed React applications a developer's delight. It's no surprise, then, that I'm very interested in bringing this delightful developer experience to Solid application development.

graphql-ld

Solid builds on RDF, a W3C-recommended model for modeling data on the World Wide Web. It's built to accommodate the reality of data interchange in a decentralized, world-wide namespace, and is therefore oriented around Universal Resource Identifiers - an idea roughly equivalent to the URLs we use to identify pages on the Web. Each datom in RDF has a subject (for instance, the URI representing me, https://tvachon.inrupt.net/profile/card#me), a predicate (say, the FOAF URI representing the idea of someone knowing someone else, https://xmlns.com/foaf/0.1/knows), and an object (i.e., the URI of my friend Toby, https://tobytoberson.inrupt.net/profile/card#me). The W3C has developed a query language called SPARQL for working with RDF datasets. SPARQL is an expressive, powerful graph query language that allows for very sophisticated querying of graph data. It's more expressive than GraphQL, but also significantly more complicated and less intuitive than the simple tree structures of GraphQL queries. This might seem like a problem for me in my desire to bridge Solid and GraphQL development, but happily I want to go in the easy direction - I'd like to be able to write queries in GraphQL and have them translated in to SPARQL in order to query the RDF stores exposed by Solid Pod servers. Delightfully, some of the core Solid contributers have been working on just such a translator - a mathematical algebra that translates GraphQL queries to SPARQL along with an implementation in TypeScript.

GraphQL-LD takes a mapping from the kinds of simple strings used in GraphQL queries to the fully-qualified URIs used in SPARQL queries in the form of a JSON-LD context:

{
    "me": "http://example.org/me",
    "name": "http://example.org/name"
}

and combines it with a graph query in GraphQL syntax:

{
  me {
    name
  }
}

to create SPARQL query:

SELECT ?me_name WHERE {
  _:b1 <http://example.org/me> ?me.
  ?me <http://example.org/name> ?me_name.
}

You can find lots of examples of how this translation works in the GitHub repository of the implementation of this translator.

The GraphQL-LD library makes it easy to use this translator to query a SPARQL query engine. From the README:

const client = new Client({ context, queryEngine });

// Define a query
const query = `
  query @single {
    label
  }`;

// Execute the query
const { data } = await client.query({ query });

This is very cool. The developers have even extended this to implement a set of React hooks and components that make it easy to build React applications that use GraphQL to query a local RDF store:

export function MyComponent() {
  const result = useQuery({ query: '{ name @single }' });
  return <span>My name is is {result.data.name}.</span>;
}

solid-apollo

The ability to use GraphQL to query a local or remote SPARQL endpoint is tantalizingly close to something I could use to build apps professionally, but I'm not sure I trust the existing store implementations to play nice with React. React applications have very specific memoization semantics and I'd like to be able to rely on a battle-tested state management solution. I'd really like to assume that the SPARQL endpoint might be slow - this both allows for local SPARQL queries that do end up taking a while and prepares us for querying remote SPARQL endpoints that will definitely be slow. Put another way, I'd like to be able to treat the GraphQL-LD Client as the network layer of an Apollo Client. Happily, Apollo Client makes this easy thanks to its Link API.

Apollo Links are "chainable 'units' that you can snap together to define how each GraphQL request is handled by your GraphQL client." They use a "middleware" pattern common in HTTP client and server libraries, and a number of useful Links are available out of the box to implement batching, query de-duplification, retries and more. Other Links can change how the query is actually executed at the network layer, for example by sending it over a websocket, and it is this type of Link we'd like to implement.

As a proof-of-concept, let's see how we could implement an Apollo Link that, given a list of RDF sources, converts a GraphQL query to SPARQL and executes that query against a local RDF store, as described by the following test:

  it('should work with an ApolloClient to execute GraphQL queries', async () => {
    const context = {
      "@context": {
        "foaf": "http://xmlns.com/foaf/0.1/",
        "name": "foaf:name"
      }
    };

    const query = gql`
  query @single(scope: all) {
    id
    name
  }`;


    const client = new ApolloClient({
      cache: new InMemoryCache({ addTypename: false }),
      link: new SolidLink(context, ["https://tvachon.inrupt.net/profile/card#me"])
    })
    const { data } = await client.query({ query })
    expect(data).toEqual({
      id: "https://tvachon.inrupt.net/profile/card#me",
      name: "Travis Vachon"
    })
  })

First, we'll extend ApolloLink and specify a constructor that accepts a JSON-LD context and a list of sources:

import { ApolloLink } from 'apollo-link';
import { JsonLdContext } from "jsonld-context-parser";

export default class SolidLink extends ApolloLink {
  constructor(context: JsonLdContext, sources: string[]) {
  }
}

In the constructor we'll create a new GraphQL-LD Client. For now we'll just use a local Client, but in the future we can refactor this to allow it to work with other query engines:

import { Client } from "graphql-ld";
import { QueryEngineComunicaSolid } from "graphql-ld-comunica-solid";
...
  constructor(context: JsonLdContext, sources: string[]) {
    super();
    this.client = new Client({ context, queryEngine: new QueryEngineComunicaSolid({ sources }) });
  }
...

Next we'll implement the request method of ApolloLink. This request method will receive an Operation object representing the request and return an Observable that Apollo Client will use to expose data to React components. You can find more information on these concepts in Apollo Client's documentation. Our implementation will use graphql/language/printer's print function to generate the GraphQL query from the AST Apollo Client uses internally and pass that to the GraphQL-LD client:

import { Observable } from 'apollo-link';
import { print } from 'graphql/language/printer';

  request(operation: Operation) {
    const runQuery = this.client.query({ query: print(operation.query) })
    ...
  }
...

Finally we'll use Promise returned by the GraphQL-LD Client to update the Observable we return from request once we have a return value or an error:

import { Observable, FetchResult } from 'apollo-link';
...
  request(operation: Operation) {
    const runQuery = this.client.query({ query: print(operation.query) })
    return new Observable<FetchResult>(observer => {
      runQuery.then(response => {
        operation.setContext({ response })
        observer.next(response)
        observer.complete()
      }).catch(err => {
        observer.error(err)
      })
    })
  }
...

Here's the whole thing:

import { ApolloLink, Observable, Operation, FetchResult } from 'apollo-link';
import { Client } from "graphql-ld";
import { QueryEngineComunicaSolid } from "graphql-ld-comunica-solid";
import { JsonLdContext } from "jsonld-context-parser";
import { print } from 'graphql/language/printer';

export default class SolidLink extends ApolloLink {
  public client: Client;
  constructor(context: JsonLdContext, sources: string[]) {
    super();
    this.client = new Client({ context, queryEngine: new QueryEngineComunicaSolid({ sources }) });
  }
  request(operation: Operation) {
    const runQuery = this.client.query({ query: print(operation.query) })
    return new Observable<FetchResult>(observer => {
      runQuery.then(response => {
        operation.setContext({ response })
        observer.next(response)
        observer.complete()
      }).catch(err => {
        observer.error(err)
      })
    })
  }
}

prs welcome

I've published this as a library and am looking for feedback from the Solid and Apollo community on its future. It may be that I'm misunderstanding something fundamental about the existing RDF stores or Apollo that make this a bad idea, but I am tentatively excited by the promise of bringing these two great tools together. I've added "enhancement" issues in the GitHub repository to help guide discussion of the future of this library, and would love your input, dear reader, if you have thoughts. Most urgently, I only have the most basic ideas about how to handle mutations - my current best guess is to provide the SolidLink with a set of mutation functions that use LDflex to implement the actual mutations, and then pass the query that makes up the mutation's "return value" to the GraphQL-LD client as we do for regular queries.

In the meantime, I'll continue prototyping and hope to use this library to advance a project that I'm hoping to write more about in the coming month. If you'd like to take this for a spin, you can

npm install solid-apollo

and pass your SolidLink'd ApolloClient to your ApolloProvider as usual:

import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client'
import { SolidLink } from 'solid-apollo'
import { InMemoryCache } from 'apollo-cache-inmemory';

const context = {
  "@context": {
    "foaf": "http://xmlns.com/foaf/0.1/",
    "name": "foaf:name",
  }
};

const client = new ApolloClient({
  cache: new InMemoryCache({ addTypename: false }),
  link: new SolidLink(context, ["https://tvachon.inrupt.net/profile/card#me"])
})

const App = () => (
  <ApolloProvider client={client}>
    <div>
      <h2>My first Apollo app</h2>
    </div>
  </ApolloProvider>
);

I've published a basic app demonstrating how to use solid-apollo on GitHub.

If you'd like to hear more, follow me on Twitter, Mastodon, or GitHub. If you'd like to join the growing Bay Area Solid community, come to the April 2020 meetup of the Bay Area Solid Interest Club.