note: the examples in this post are no longer live, and the data access patterns no longer recommended. for updates on Solid development, check out my work at Understory
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.