GraphQL Resolvers + Ramda = CRUD

Yazeed Bzadough
Frontend Weekly
Published in
6 min readMay 13, 2018

I began learning GraphQL and already love how it compliments Redux by shaping API responses without actions/reducers. Writing resolvers feels a bit like writing reducers, which I already love doing with Ramda.

I’m currently following this amazing GraphQL tutorial, and wish to discuss implementing its exercises with Ramda.

Disclaimer:

These patterns are intentionally overkill, and only meant to have some Ramda fun 🐏. You’ll learn some basic GraphQL if you haven’t already. 😁

Setup

  • Clone this repo
  • Checkout the start branch
  • Run npm i && npm start
  • Go to http://localhost:4000

Schema

src/schema.graphql looks like this. You can only get all links for now.

type Query {
links: [Link!]
}
type Link {
id: ID!
description: String!
url: String!
}

src/links.json is based on the howtographql tutorial, just duplicated a few times for more sample data.

[
{
"id":"link-0",
"url":"www.howtographql.com",
"description":"Fullstack tutorial for GraphQL"
},
{
"id":"link-1",
"url":"www.howtographql.com",
"description":"Fullstack tutorial for GraphQL"
},
{
"id":"link-2",
"url":"www.howtographql.com",
"description":"Fullstack tutorial for GraphQL"
},
{
"id":"link-3",
"url":"www.howtographql.com",
"description":"Fullstack tutorial for GraphQL"
}
]

Here’s src/index.js

const { GraphQLServer } = require('graphql-yoga');
let links = require('./links.json');
const resolvers = {
Query: {
links: () => links
}
};
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers
});
server.start(() => console.log('Running on port 4000'));

Since our links resolver returns the links array, querying for it returns the entire dataset.

Query:

query {
links {
id
url
description
}
}

Find by ID

Let’s update src/schema.graphql and allow finding links by ID.

type Query {
links: [Link!]
link(id: ID!): Link
}

Now our resolver in src/index.js

const resolvers = {
Query: {
links: () => links,
link: (root, { id }) => links.find((link) => link.id === id)
}
};

Search links with the given id.

Try this query:

query {
link(id: "link-2") {
id
url
description
}
}

Our result:

{
"data": {
"link": {
"id": "link-2",
"url": "www.howtographql.com",
"description": "Fullstack tutorial for GraphQL"
}
}
}

Works perfectly! Feel free to try other IDs.

R.find and R.propEq

Much like the native Array.find, R.find returns the first element matching your predicate function.

So we could refactor our link resolver to

const { find } = require('ramda');const resolvers = {
Query: {
links: () => links,
link: (root, { id }) => find((link) => link.id === id, links)
}
};

But that’s not exciting enough. We can replace the predicate with R.propEq.

const { find, propEq } = require('ramda');
const idEq = propEq('id');
const resolvers = {
Query: {
links: () => links,
link: (root, { id }) => find(idEq(id), links)
}
};

R.propEq takes 3 parameters:

  1. Property name
  2. A value
  3. The object to match on

Since it’s curried, we can supply one or two params and get back a function expecting the rest. This makes partial application trivial.

We supplied 'id' as the property name to look for, then id from the link resolver as the value, and find will supply each link object as it loops over the list.

Our query results haven’t changed.

Mutation: Add new links

Let’s update src/schema.graphql and support adding new link resources.

type Mutation {
post(url: String!, description: String!): Link!
}

We require a url and description, and will return the new link upon creating it.

Now we add a post resolver.

const resolvers = {
Query: {
links: () => links,
link: (root, { id }) => find(propEq('id', id), links)
},
Mutation: {
post: (root, { url, description }) => {
const link = {
id: `link-${links.length}`,
url,
description
};
links.push(link); return link;
}
}

};

Try this query:

mutation {
post(url: "website", description: "lol") {
id
url
description
}
}

Our result:

R.merge, R.pick, and R.pipe

I think using Ramda here is overkill, but let’s experiment!

  • R.merge merges two objects
  • R.pick returns a shallow copy of an object’s chosen keys
  • R.pipe will allow merge and pick to beautifully flow, left-to-right

For more detail on pipe, see my article on it!

const { merge, pick, pipe } = require('ramda');Mutation: {
post: (root, args) => pipe(
pick(['url', 'description']),
merge({ id: `link-${links.length}` }),
(link) => {
// OMG side-effect! O_o"
links.push(link);
return link;
}
)(args)
}

pick returns { url, description }, merge fuses it with an object containing the new id, and our last arrow function returns the new link after pushing it into the links array.

Each function’s output is supplied to the next via pipe!

Amazingly, our query results haven’t changed.

Mutation: Updating Links

We’ve fulfilled the Create and Read portions of CRUD, now let’s do Update.

Edit src/schema.graphql

type Mutation {
post(url: String!, description: String!): Link!
updateLink(id: ID!, url: String, description: String): Link
}

We require an ID and optionally take the new url and description. Updated link is returned (if found).

Now src/index.js

We’ve already covered the pattern of matching objects by ID, so we can reuse idEq here.

Mutation: {
post: ...
updateLink: (root, args) => {
let newLink;
links = links.map((link) => {
if (idEq(link.id, args)) {
newLink = { ...link, ...args };
return newLink;
}
return link;
});
return newLink;
}

}

Try this query:

mutation {
updateLink(id: "link-0" url: "https://bit.ly/2IzZV4C") {
id
url
description
}
}

Successfully updated!

R.when and R.merge

updateLink's mapping function had an if without an else.

if (idEq(link.id, args)) {
newLink = { ...link, ...args };
return newLink;
}
return link;

R.when is a great function to express that logic.

doubleSmallNums = when(
(num) => num < 10,
(num) => num * 2
);
doubleSmallNums(9); // 18
doubleSmallNums(10); // 10

If the first function returns true, run the second function.
Else, do nothing.

See my article on when()for more info.

Spoiler alert: We’ll be using when to delete links too, so let’s abstract it right now.

const { when } = require('ramda');
const doIfMatchingId = (id) => when(idEq(id));
updateLink: (root, args) => {
let newLink;
const updateLink = (link) => {
newLink = merge(link, args);
return newLink;
};
links = links.map(doIfMatchingId(args.id)(updateLink)); return newLink;
}

Our new logic reads like a sentence: “When args and link IDs are equal, create and return newLink.”

We don’t even need to specify “otherwise, do nothing” because when handles that for us!

Our query results haven’t changed.

Mutation: Deleting Links

Let’s finish off CRUD and implement Delete!

Edit src/schema.graphql

type Mutation {
post(url: String!, description: String!): Link!
updateLink(id: ID!, url: String, description: String): Link
deleteLink(id: ID!): Link
}

We’ll delete and return the link if we can find it by ID.

Now src/index.js

deleteLink: (root, { id }) => {
let linkToDelete;
links.forEach((link, index) => {
const matchAndRemove = (match) => {
linkToDelete = match;
links.splice(index, 1);
};
return doIfMatchingId(id)(matchAndRemove, link);
});
return linkToDelete;
}

Try this query:

mutation {
deleteLink(id: "link-1") {
id url description
}
}

We get the expected response:

{
"data": {
"deleteLink": {
"id": "link-1",
"url": "www.howtographql.com",
"description": "Fullstack tutorial for GraphQL"
}
}
}

Then try this query:

query {
link(id: "link-1") {
id url description
}
}

And get nothing back, if everything worked out:

{
"data": {
"link": null
}
}

Again, this is intentionally overkill.

Ramda truly shines when you enforce immutable data structures. Here, it’s at least intriguing and helping us to think laterally because it’s such a flexible library.

Find anything interesting? How can we improve this? Leave a comment below and let’s discuss.

Until next time!

Take care,
Yazeed Bzadough

--

--