Graphql CRUD operations on a CouchDB database with nodejs.

Graphql CRUD operations on a CouchDB database with nodejs.

Learn how to perform basic CRUD (Create, Read, Update, and Delete) operations on a Couchdb database through a Graphql API.

Prerequisites

  • Basic knowledge of typescript & javascriptt
  • Basic knowledge of graphql
  • Good understanding of Nodejs
  • Knowledge of couchdb is an advantage
  • You must have Nodejs installed on your computer
  • You must have couchdb installed on your computer https://couchdb.apache.org/

CouchDB

CouchDB falls under the document-oriented database in the NoSQL landscape and it is known for its ease of use and scalable architecture. It is highly available and partition tolerant but is also eventually consistent. Thus, it is an AP based system according to CAP (Consistency, Availability, and Partition Tolerance) theorem for distribute database systems.

Note: This write-up doesn’t discuss the suitability of the CouchDB to various use-cases, rather emphasizes on connecting to the database and interacting with it by performing CRUD operations through a Graphql API.

Architecture

CouchDB organizes data into multiple databases. A database is a collection of documents, and each document is a JSON object. As CouchDB stores data in the form of JSON documents, it is schema-less and highly flexible.

Each document in a database contains a bare minimum of two fields: _id which represents unique identity of the document, and _rev which represents the revision number of the document. If the document posted while document creation doesn’t have _id attribute, CouchDB generates one and saves the document. On the other hand, _rev is used to resolve document update conflict. If two clients tries to update the same document, the first update wins and the second one has to get the update from first client before it’s update.

Creating a database

CouchDB installation come up with a web administration console and can be accessible from http://localhost:5984/_utils. This page lists all the databases available in the running CouchDB instance.

Couchdb database.

Click of the Create Database to manually create a database.

Nano is a greate tool for communicating with our CouchDB database, However, It has one of the most terrible documentaions expecially when it commes to Typescript.

Without further ado, Lets get into the coding part πŸš€.

The first step is to install all the necessary dependencies. I'll be using Yarn throughout this tutorial.

$

bash

yarn add graphql-yoga nano dotenv;
  • graphql-yoga: Fully-featured GraphQL Server with focus on easy setup, performance & great developer experience
  • dotenv: Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env.
  • nano: Official Apache CouchDB library for Node.js.

Dev Dependencies

$

bash

yarn add -D typescript ts-node @types/node nodemon

Our project structure

└── src
└── dbConnection
└── couch.ts
β”œβ”€β”€ index.js
β”œβ”€β”€ resolvers.js
└── typeDefs.js
β”œβ”€β”€ package.json
└── .env

Code walkthrough

This section describes the application code in a bottom-up fashion.

1: Setting up Graphql Server

Your src/index.ts should look like:

ts

ts

require("dotenv").config(); // For environment variables
import { GraphQLServer } from "graphql-yoga";
import { Server } from "http";
import { Server as HTTPSServer } from "https";
import typeDefs from "./typeDefs"; // we are going to create this in a minute
import resolvers from "./resolvers"; // we are going to create this in a minute
export default (async (): Promise<Server | HTTPSServer> => {
const server = new GraphQLServer({
typeDefs,
resolvers,
});
const port = process.env.PORT || 4000;
return await server.start(
{
port,
},
() => console.log(`server is running on http://localhost:${port}`)
);
})();

2: Creating an instance of Nano

Add the following snippet to your ./dbConnection/couch.ts file:

ts

ts

import * as Nano from "nano";
export default (async () => {
const dbName = "hello";
const nano = Nano(process.env.DB_HOST_AUTH); // I'll be storing the database connection uri
// in an environment variable since it contains vital credentials.
const dbList = await nano.db.list(); // Returns a list of database
try {
if (!dbList.includes(dbName)) {
// create a new DB if database doesn't exist.
await nano.db.create(dbName);
const db = nano.use(dbName);
console.log("database created successfully");
return db;
} else {
const db = nano.use(dbName);
console.log("connected to database successfully");
return db;
}
} catch (err) {
throw new Error(err);
}
})();

The above code snippet first retrieves all the database names in our couchDB then checks if it includes a the database we want to use and then uses it with the nano.use(dbName) function. If it doesn't include the our database name we want to use then it will automatically create a new database with the given name.

Nano(process.env.DB_HOST_AUTH) receives a connection string which varies depending on wether we require authentication or not.

  • http://username:password@localhost:5984 includes credentials thus stored in the .env file as DB_HOST_AUTH=http://username:password@localhost:5984
  • http://localhost:5984 does not include any credentials and can be used directly.

3: Graphql Type Definitions

Add the following code to your src/typeDefs.ts file:

ts

ts

export default `
type Doc {
name: String!
email: String!
age: Int!
nice: Boolean!
updated: Boolean
}
type Mutation {
createRecord(name: String!, email: String!, age: Int!, nice: Boolean!): Boolean!
delete(id: String, rev: String): Boolean!
update(id: String, rev: String, updated: Boolean): Boolean!
}
type Query {
findAll: [Doc!]
findSingle(id: String!): Doc!
}
`;

I won't go in detail on how graphql works like in Mutations and Queries as this is not a Beginners Graphql guide.

4: Resolvers.

Resolvers are per field functions that are given a parent object, arguments, and the execution context, and are responsible for returning a result for that field. Resolvers cannot be included in the GraphQL schema language, so they must be added separately. The collection of resolvers is called the "resolver map". It mostly consist of Queries and Mutations.

Mutations -

4a: Creating a record - nano.insert().

The first operation in CRUD is Create. nano.insert() is used to both insert and update the document. This function takes either an object or a string as an argument and inserts/updates the document given.

ts

ts

import { MaybeDocument } from "nano";
import couch from "./dbConnection/couch";
// Lets define the interfaces for each resolver.
interface User extends MaybeDocument {
name: string;
email: string;
age: number;
nice: boolean;
}
interface Update extends MaybeDocument {
updated: boolean;
id: string;
rev: string;
}
export default {
Mutation: {
createRecord: async (_parent: any, args: User) => {
try {
const record = await (await couch).insert(args);
console.log(record);
return true;
} catch (err) {
console.log(err);
return false;
}
},
},
};

Creating a record


4b: Update a record - nano.insert(id, rev).

As stated earlier, nano.insert() is used to both insert and update the document. When this function has given a document with both _id and _rev, this function performs an update. If the _rev given in the document is obsolete, update fails and the client is expected to get the latest revision of the document before performing any further updates

Below code demonstrates retrieving a blog by it’s id.

ts

ts

...
export default {
Mutation: {
update: async (_: any, { id, rev, ...args }: Update) => {
const findFile = await (await couch).get(id);
if (findFile) {
const file = await (await couch).insert({
_id: id,
_rev: rev,
...findFile,
...args,
});
console.log(file);
return true;
}
return false;
},
...
},
};

Update a record


4c: Delete record - `nano.destroy(id, rev).

nano.destroy(id, rev, [callback]) is used delete a document from database. Underneath method deletes a blog entry given it’s _id and _rev

The Nano delete function requires a document _id and a _rev

Below code demonstrates deletion of a record by its id and rev.

ts

ts

...
export default {
Mutation: {
delete: async (_: any, { id, rev }: { id: string; rev: string }) => {
const record = await (await couch).destroy(id, rev);
console.log(record);
return true;
},
...
},
};

Delete a record


4d 1: Retrieve a record by id - `nano.get(id).

nano.get(id, [params], [callback]) is used to get the document by its id. Underneath method in BlogService class gets the blog given its id.

Below code demonstrates retrieving a document by it’s id.

ts

ts

...
export default {
Query: {
findSingle: async (_: any, { id }: { id: string }) => {
const file = await (await couch).get(id);
console.log(file);
return file;
},
...
},
};

Retrieve a record


4d 2: Retrieve multiple files - `nano.find(selector).

nano.find(selector, [callback]) performs a "Mango" query by supplying a JavaScript object containing a selector: the fields option can be used to retrieve specific fields.

Below code demonstrates how to retrieve documents from couchdb.

ts

ts

...
export default {
Query: {
findAll: async () => {
const files = await (await couch).find({
selector: {}, // parameters can be added to query specific documents.
fields: ['name', 'email', 'age', 'nice', 'updated'],
});
console.log(files.docs);
return files.docs;
},
...
},
};

Retrieve multiple records



Your final resolvers.ts file should not be different from the below code:

ts

ts

import { MaybeDocument } from "nano";
import couch from "./dbConnection/couch";
interface User extends MaybeDocument {
name: string;
email: string;
age: number;
nice: boolean;
}
interface Update extends MaybeDocument {
updated: boolean;
id: string;
rev: string;
}
export default {
Mutation: {
createRecord: async (_parent: any, args: User) => {
try {
const record = await (await couch).insert(args);
console.log(record);
return true;
} catch (err) {
console.log(err);
return false;
}
},
delete: async (_: any, { id, rev }: { id: string; rev: string }) => {
const record = await (await couch).destroy(id, rev);
console.log(record);
return true;
},
update: async (_: any, { id, rev, ...args }: Update) => {
const findFile = await (await couch).get(id);
if (findFile) {
const file = await (await couch).insert({
_id: id,
_rev: rev,
...findFile,
...args,
});
console.log(file);
return true;
}
return false;
},
},
Query: {
findAll: async () => {
const files = await (await couch).find({
selector: {},
fields: ["name", "email", "age", "nice", "updated"],
});
console.log(files.docs);
return files.docs;
},
findSingle: async (_: any, { id }: { id: string }) => {
const file = await (await couch).get(id);
console.log(file);
return file;
},
},
};

You can find the entire code for this article on my github repo https://github.com/DNature/couchdb-graphql

Conclusion:

To conclude, the blog has discussed CouchDB basics, and explained how to perfrom CRUD operations on a CouchDB database using Node, Graphql and Nano.



I hope you find this helpful.

Happy codding πŸ’» πŸ™‚