If you had the pleasure of joining us last time, we had just completed a crash course in structuring GraphQL Queries. As much as we all love studying abstract queries within the confines of a playground environment, the only real way to learn anything to overzealously attempt to build something way out of our skill level. Thus, we're going to shift gears and actually make something with all the dry technical knowledge we've accumulated so far. Hooray!

Data Gone Wild: Exposing Your GraphQL Endpoint

If you're following along with Prisma as your GraphQL service, the endpoint for your API defaults to [your_ip_address]:4466. What's more, you've probably noticed it is publicly accessible. THIS IS VERY BAD. Your server has full read/write access to whichever database you configured with it... if anybody finds your endpoint hanging out in a Github commit somewhere, you've just lost your database and everything in it. You're pretty much Equifax, and you should feel bad.

Prisma has a straightforward solution. While SSHed into wherever-you-set-up-your-server, check out the prisma.yaml file generated when we first started getting set up. You know, this directory:

/my-prisma
├── datamodel.prisma
├── docker-compose.yml
├── generated
│   └── prisma-client
│       ├── index.ts
│       └── prisma-schema.ts
└── prisma.yml

The project structure of a new Prisma

prisma.yaml seems inglorious, but that's because it's hiding a secret... or should I say, it's not hiding a secret! Hah!... (you know, like, credentials). Anyway.

To enable authorization on our endpoint, we need to add a secret to our prisma.yaml file. The secret can be anything you like; this is simply a string used to generate a token. Add a line that defines secret like this:

endpoint: https://localhost:4466
datamodel: datamodel.prisma
secret: HIIHGUTFTUY$VK$G$YI&TUYCUY$DT$

generate:
  - generator: typescript-client
    output: ./generated/prisma-client/

Adding a random string to serve as a secret token to prisma.yaml

With your secret stashed away safely, the Prisma CLI can now use this secret to create the authentication token. This will be the value we pass in the headers of our requests to interact with our Prisma server remotely.

Type $ prisma token in your project directory to get the work of art:

$ prisma token
eyJhbGciOiJIUzI1NiIsInUYGFUJGSFKHFGSJFKSFJKSFGJdfSwiaWF0IjoxNTUyMTYwMDQ5LCJleHAiOjE1NTI3NjQ4NDl9.xrubUg_dRc93bqqR4f6jGt-KvQRS2Xq6lRi0a0uw-C0

Secret token generated from your random string

Nice; believe it or not, that was the "hard" part.

EXTRA CREDIT: Assign a DNS Record and Apply a Security Certificate

If you really want to, you could already query against your insecure IP address and start receiving some information. That said, making HTTP requests as such from HTTPS origins will fail. Not only that but you kind of look shitty for not even bothering to name your API, much less apply for a free SSL certificate. For the easiest way to do this, see our post on using Caddy as an HTTP server.

Building a Javascript Client to Consume Our API

With our API nice and secure, we can start hitting this baby from wherever we want... as long as it's a Node app. We'll start by requiring two packages:

const { GraphQLClient } = require('graphql-request')
const { dotenv } = require('dotenv').config()

GraphQLClient is the magic behind our client- it's everything. It is also very similar to existing npm libraries for making requests, such as node-fetch.

We'll also leverage the dotenv library to ensure our API endpoint and Bearer token stay out of source code. Try not to be Equifax whenever possible. dotenv allows us to load sensitive values from a .env file. Just in case you need a refresher, that file should look like this:

NODE_ENV=Production
ENDPOINT=https://yourapiendpoint.com
AUTH=Bearer eyJhbGciOBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHGUYFIERIBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHZl-UGnMrOk3w

Initialize The GraphQL Client

I like to set up a one-time client for our API that we can reuse if needed. After pulling the API endpoint and token from our .env file, setting up the client is easy:

const { GraphQLClient } = require('graphql-request')
const { dotenv } = require('dotenv').config()

const endpoint = process.env.ENDPOINT;
const token = process.env.AUTH;

// Initialize GraphQL Client
const client = new GraphQLClient(endpoint, {
  headers: {
    Authorization: token
  }
});

Initializing a GraphQL Client in Javascript

EMERGENCY MEETING: EVERYBODY HUDDLE UP

I'm sorry, were you focusing on development? Unfortunately for you, I spent 8 years as a product manager, and I love stopping everything suddenly to call emergency meetings.

Real talk, though; let's think back to the JIRA Kanban board example we've been using for the last two posts. If you recall, we will write a query that populates a 4-column Kanban board. The board represents a project (in this case, Hackers and Slackers) and each column represents the status of a ticket, like this:

const statuses = ['Backlog', 'To Do', 'In Progress', 'Done'];

List of JIRA statuses

We've previously established that GraphQL queries are friendly to drop-in variables. Let's use this to build some logic into our client instead of hardcoding a massive query, which is really just the same 4 queries stitched together. Here's what a query to populate a single JIRA column looks like:

const query = `
    query JiraIssuesByStatus($project: String, $status: String) {
         jiraIssues(where: {project: $project, status: $status}, 
         orderBy: timestamp_DESC, 
         first: 6) {
            key
            summary
            epic
            status
            project
            priority
            issuetype
            timestamp
            }
         }`

Structured GraphQL Query to fetch JIRA issues by status

We're passing both the project and the issue status as variables to our query. We can make things a bit dynamic here by looping through our statuses and executing this query four times: each time resulting in a callback filling the appropriate columns with JIRA issues.

💡
This approach is certainly less clunky and more dynamic than a hardcoded query. That said, this still isn't the best solution. Remember: the strength of GraphQL is the ability to get obscene amounts of data across complex relationships in a single call. The best approach here would probably be to dynamically build the query string using fragments, which we'll review in the next post.

Game On: Our Client in Action

const { GraphQLClient } = require('graphql-request')
const { dotenv } = require('dotenv').config()

const endpoint = process.env.ENDPOINT;
const token = process.env.AUTH;

// Initialize GraphQL Client
const client = new GraphQLClient(endpoint, {
  headers: {
    Authorization: token
  }
});

// Structured query
const query = `
   query JiraIssuesByStatus($project: String, $status: String) {
      jiraIssues(where: {project: $project, status: $status}, orderBy: timestamp_DESC, first: 6) {
         key
         summary
         epic
         status
         project
         priority
         issuetype
         timestamp
        }
      }
    `;

// All Possible Issue Statuses
const statuses = ['Backlog', 'To Do', 'In Progress', 'Done'];

// Execute a query per issue status
for(var i = 0; i < statuses.length; i++){
  var variables = {
    project: "Hackers and Slackers",
    status: statuses[i]
  }

  client.request(query, variables).then((data) => {
    console.log(data)
  }).catch(err => {
    console.log(err.response.errors) // GraphQL response errors
    console.log(err.response.data) // Response data if available
  });
}

Works like a charm. We only had one endpoint, only had to set one header, and didn't spend time reading through hundreds of pages of documentation to figure out which combination of REST API endpoint, parameters, and methods actually got us what we wanted. It's almost as if we're writing SQL now, except... it looks a lot more like... NoSQL. Thanks for the inspiration, MongoDB! Hope that whole selling-open-source-software thing works out.

Oh, and of course, here were the results of my query:

{ jiraIssues:
   [ { priority: 'Medium',
       timestamp: 1550194791,
       project: 'Hackers and Slackers',
       key: 'HACK-778',
       epic: 'Code snippets',
       status: 'Backlog',
       issuetype: 'Task',
       summary: 'HLJS: set indentation level' },
     { priority: 'Medium',
       timestamp: 1550194782,

       project: 'Hackers and Slackers',
       key: 'HACK-555',
       epic: 'Optimization',
       status: 'Backlog',
       issuetype: 'Task',
       summary: 'Minify Babel' },
     { priority: 'Medium',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-785',
       epic: 'New Post',
       status: 'Backlog',
       issuetype: 'Task',
       summary: 'Unix commands for data' },
     { priority: 'Medium',
       timestamp: 1550016000,
       project: 'Hackers and Slackers',
       key: 'HACK-251',
       epic: 'New Post',
       status: 'Backlog',
       issuetype: 'Content',
       summary: 'Using Ghost\'s content filtering' },
     { priority: 'Medium',
       timestamp: 1550016000,
       project: 'Hackers and Slackers',
       key: 'HACK-302',
       epic: 'Widgets',
       status: 'Backlog',
       issuetype: 'Integration',
       summary: 'Discord channel signups ' },
     { priority: 'Low',
       timestamp: 1550016000,
       project: 'Hackers and Slackers',
       key: 'HACK-336',
       epic: 'New Post',
       status: 'Backlog',
       issuetype: 'Content',
       summary: 'Linux: Configuring your server to send SMTP emails' } ] }
{ jiraIssues:
   [ { priority: 'Medium',
       timestamp: 1550224412,
       project: 'Hackers and Slackers',
       key: 'HACK-769',
       epic: 'Projects Page',
       status: 'Done',
       issuetype: 'Bug',
       summary: 'Fix projects dropdown' },
     { priority: 'High',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-710',
       epic: 'Lynx',
       status: 'Done',
       issuetype: 'Task',
       summary: 'Implement auto text synopsis for Lynx posts' },
     { priority: 'Medium',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-777',
       epic: 'Creative',
       status: 'Done',
       issuetype: 'Task',
       summary: 'Redesign footer to be informative; link-heavy' },
     { priority: 'Highest',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-779',
       epic: 'Urgent',
       status: 'Done',
       issuetype: 'Task',
       summary: 'Changeover from cloudinary to DO' },
     { priority: 'Medium',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-780',
       epic: 'Creative',
       status: 'Done',
       issuetype: 'Task',
       summary: 'Make mobile post title bold' },
     { priority: 'High',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-781',
       epic: 'Urgent',
       status: 'Done',
       issuetype: 'Bug',
       summary: 'This post consistently doesn’t work on mobile' } ] }
{ jiraIssues:
   [ { priority: 'Low',
       timestamp: 1550223282,
       project: 'Hackers and Slackers',
       key: 'HACK-782',
       epic: 'Widgets',
       status: 'To Do',
       issuetype: 'Task',
       summary:
        'Lynx: on mobile, instead of full link, show domainname.com/...' },
     { priority: 'High',
       timestamp: 1550194799,
       project: 'Hackers and Slackers',
       key: 'HACK-774',
       epic: 'Widgets',
       status: 'To Do',
       issuetype: 'Task',
       summary: 'New Widget: Next/Previous article in series' },
     { priority: 'Low',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-395',
       epic: 'Page Templates',
       status: 'To Do',
       issuetype: 'Task',
       summary: 'Create fallback image for posts with no image' },
     { priority: 'High',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-756',
       epic: 'Newsletter',
       status: 'To Do',
       issuetype: 'Major Functionality',
       summary: 'Automate newsletter' },
     { priority: 'Low',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-775',
       epic: 'Projects Page',
       status: 'To Do',
       issuetype: 'Data & Analytics',
       summary: 'Update issuetype icons' },
     { priority: 'Lowest',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-776',
       epic: 'Projects Page',
       status: 'To Do',
       issuetype: 'Task',
       summary: 'Add fork icon to repos' } ] }
{ jiraIssues:
   [ { priority: 'High',
       timestamp: 1550102400,
       project: 'Hackers and Slackers',
       key: 'HACK-784',
       epic: 'New Post',
       status: 'In Progress',
       issuetype: 'Content',
       summary: 'Welcome to SQL part1' } ] }

The output of the GraphQL post

Before we say "GG, 2ez, 1v1 me," know that we're only getting started uncovering what GraphQL can do. It's not all just creating and deleting records either; we're talking full-on database JOIN equivalent type shit here. Stick around, folks; the bandwagon's just getting warmed up.