Code Helper Guide for "GraphQL in Action"

Part 1

Chapter 1

Listing 1.13 - GraphQL query for the Star Wars example
{
  person(personID: 4) {
    name
    birthYear
    homeworld {
      name
    }
    filmConnection {
      films {
        title
      }
    }
  }
}

Chapter 2

Listing 2.1 - Query for person
{
  person(personID: 4) {
    name
    birthYear
  }
}
Listing 2.9 - Query for the last 10 repositories for logged-in user
{
  viewer {
    repositories(last: 10) {
      nodes {
        name
        description
      }
    }
  }
}
Listing 2.10 - Query for all GitHub-supported licenses
{
  licenses {
    name
    url
  }
}
Listing 2.11 - Query for the first 10 issues of a repository
{
  repository(owner: "facebook", name: "graphql") {
    issues(first: 10) {
      nodes {
        title
        createdAt
        author {
          login
        }
      }
    }
  }
}
Listing 2.12 - Mutation to "star" a repository
mutation {
  addStar(input: {starrableId: "MDEwOlJlcG9zaXRvcnkzODM0MjIyMQ=="}) {
    starrable {
      stargazers {
        totalCount
      }
    }
  }
}
Listing 2.13 - Query to find an id of a repository
{
  repository(name: "graphql", owner: "facebook") {
    id
  }
}
Listing 2.14 - Query for the details of one issue under a repository
query GetIssueInfo {
  repository(owner: "jscomplete", name: "graphql-in-action") {
    issue(number: 1) {
      id
      title
    }
  }
}
Listing 2.15 - Mutation to add a comment to a repository issue
mutation AddCommentToIssue {
  addComment(input: {
    subjectId: "MDU6SXNzdWUzMDYyMDMwNzk=",
    body: "Hello GraphQL"
  }) {
    commentEdge {
      node {
        createdAt
      }
    }
  }
}
Listing 2.16 - Example GraphQL introspective query
{
  __schema {
    types {
      name
      description
    }
  }
}
Listing 2.17 - Query for supported fields under a Commit object
{
  __type(name: "Commit") {
    fields {
      name
      args {
        name
      }
    }
  }
}

Chapter 3

Listing 3.3 - Query for one organization information
query OrgInfo {
  organization(login: "facebook") {
    name
    description
    websiteUrl
  }
}
Listing 3.4 - Query for first 10 repositories under organization
query First10Repos {
  organization(login: "facebook") {
    name
    description
    websiteUrl
    repositories(first: 10) {
      nodes {
        name
      }
    }
  }
}
Listing 3.5 - Query for first 10 alphabetically-ordered repositories under organization
query orgReposByName {
  organization(login: "facebook") {
    repositories(first:10, orderBy:{field: NAME, direction: ASC}) {
      nodes {
        name
      }
    }
  }
}
Listing 3.6 - Query for top-10 most popular repositories under organization
query OrgPopularRepos {
  organization(login: "facebook") {
    repositories(first:10, orderBy:{field: STARGAZERS, direction: DESC}) {
      nodes {
        name
      }
    }
  }
}
Listing 3.7 - Query example for working with cursors under edges
query OrgRepoConnectionExample {
  organization(login: "facebook") {
    repositories(first:10, orderBy:{field: STARGAZERS, direction: DESC}) {
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}
Listing 3.8 - Query example to fetch second page of popular repositories
query OrgPopularReposPage2 {
  organization(login: "facebook") {
    repositories(
      first: 10,
      after: "Y3Vyc29yOnYyOpLNOE7OAeErrQ==",
      orderBy: {field: STARGAZERS, direction: DESC}
    ) {
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}
Listing 3.9 - Query example for meta pagination information
query OrgReposMetaInfoExample {
  organization(login: "facebook") {
    repositories(
      first: 10,
      after: "Y3Vyc29yOnYyOpLNOE7OAeErrQ==",
      orderBy: {field: STARGAZERS, direction: DESC}
    ) {
      totalCount
      pageInfo {
        hasNextPage
      }
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}
Listing 3.10 - Query example for using field arguments to search
query SearchExample {
  repository(owner: "twbs", name: "bootstrap") {
    projects(search: "v4.1", first: 10) {
      nodes {
        name
      }
    }
  }
}
Listing 3.11 - Query example for using field arguments to filter
query FilterExample {
  viewer {
    repositories(first: 10, affiliations: OWNER) {
      totalCount
      nodes {
        name
      }
    }
  }
}
Listing 3.12 - Example for using arguments to provide input values to a mutation
mutation StarARepo {
  addStar(input: {starrableId: "MDEwOlJlcG9zaXRvcnkzODM0MjIyMQ=="}) {
    starrable {
      stargazers {
        totalCount
      }
    }
  }
}
Listing 3.13 - Profile information query
query ProfileInfo {
  user(login: "samerbuna") {
    name
    company
    bio
  }
}
Listing 3.14 - Profile information query with alias
query ProfileInfoWithAlias {
  user(login: "samerbuna") {
    name
    companyName: company
    bio
  }
}
Listing 3.15 - Query for all supported directives
query AllDirectives {
  __schema {
    directives {
      name
      description
      locations
      args {
        name
        description
        defaultValue
      }
    }
  }
}
Listing 3.16 - Using variables for argument values
query OrgInfo($orgLogin: String!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl
  }
}
Listing 3.17 - Using default values for variables
query OrgInfoWithDefault($orgLogin: String = "facebook") {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl
  }
}
Listing 3.18 - Example for using the @include directive
query OrgInfo($orgLogin: String!, $fullDetails: Boolean!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl @include(if: $fullDetails)
  }
}
Listing 3.19 - Example for using the @skip directive
query OrgInfo($orgLogin: String!, $partialDetails: Boolean!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl @skip(if: $partialDetails)
  }
}
Listing 3.20 - Using @include and @skip together
query OrgInfo($orgLogin: String!, $partialDetails: Boolean!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl @skip(if: $partialDetails) @include(if: false)
  }
}
Listing 3.24 - Example for a query with repeated sections
query MyRepos {
  viewer {
    ownedRepos: repositories(affiliations: OWNER, first: 10) {
      nodes {
        nameWithOwner
        description
        forkCount
      }
    }
    orgsRepos: repositories(affiliations: ORGANIZATION_MEMBER, first: 10) {
      nodes {
        nameWithOwner
        description
        forkCount
      }
    }
  }
}
Listing 3.25 - Using fragments to minimize repetitions in GraphQL operations
query MyRepos {
  viewer {
    ownedRepos: repositories(affiliations: OWNER, first: 10) {
      ...repoInfo
    }
    orgsRepos: repositories(affiliations: ORGANIZATION_MEMBER, first: 10) {
      ...repoInfo
    }
  }
}

fragment repoInfo on RepositoryConnection {
  nodes {
    nameWithOwner
    description
    forkCount
  }
}
Listing 3.34 - Inline fragment example
query InlineFragmentExample {
  repository(owner: "facebook", name: "graphql") {
    ref(qualifiedName: "master") {
      target {
        ... on Commit {
          message
        }
      }
    }
  }
}
Listing 3.35 - Example for a GraphQL union type
query RepoUnionExample {
  repository(owner: "facebook", name: "graphql") {
    issueOrPullRequest(number: 3) {
      __typename
    }
  }
}
Listing 3.36 - Using inline fragments with union types
query RepoUnionExampleFull {
  repository(owner: "facebook", name: "graphql") {
    issueOrPullRequest(number: 5) {
      ... on PullRequest {
        merged
        mergedAt
      }
      ... on Issue {
        closed
        closedAt
      }
    }
  }
}
Listing 3.37 - Query a union-type search field
query TestSearch {
  search(first: 100, query: "graphql", type: USER) {
    nodes {
      ... on User {
        name
        bio
      }
      ... on Organization {
        login
        description
      }
    }
  }
}

Part 2

Chapter 4

Listing 4.30 - The "azdev.users" table
CREATE TABLE azdev.users (
  id serial PRIMARY KEY,

  email text NOT NULL UNIQUE,
  hashed_password text NOT NULL,

  first_name text,
  last_name text,

  is_admin boolean NOT NULL DEFAULT false,

  hashed_auth_token text,

  created_at timestamp without time zone NOT NULL
    DEFAULT (now() at time zone 'utc'),

  CHECK (lower(email) = email)
);
Listing 4.32 - The Needs table
CREATE TABLE azdev.needs (
  id serial PRIMARY KEY,

  content text NOT NULL,
  tags text,

  user_id integer NOT NULL,

  is_public boolean NOT NULL,
  is_featured boolean NOT NULL DEFAULT FALSE,

  created_at timestamp without time zone NOT NULL
    DEFAULT (now() at time zone 'utc'),

  FOREIGN KEY (user_id) REFERENCES azdev.users,
  CHECK (is_public = true OR is_featured = false)
);
Listing 4.33 - The Approaches table
CREATE TABLE azdev.approaches (
  id serial PRIMARY KEY,

  content text NOT NULL,

  user_id integer NOT NULL,
  need_id integer NOT NULL,

  vote_count integer NOT NULL DEFAULT 0,

  created_at timestamp without time zone NOT NULL
    DEFAULT (now() at time zone 'utc'),

  FOREIGN KEY (user_id) REFERENCES azdev.users,
  FOREIGN KEY (need_id) REFERENCES azdev.needs
);
Listing 4.35 - The "approachDetails" collection
db.createCollection("approachDetails", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["pgId"],
      properties: {
        pgId: {
          bsonType: "int",
          description: "must be an integer and is required"
        },
      }
    }
  }
});
Listing 4.36 - Initial design of the AZdev schema
type User implement Node {
  id: ID!
  createdAt: String!
  email: String!
  name: String!
  needList: [Need!]!
}

enum ApproachDetailCategory {
  NOTE
  EXPLANATION
  WARNING
}

type ApproachDetail {
  content: String!
  category: ApproachDetailCategory!
}

type Approach implement Node {
  id: ID!
  createdAt: String!
  content: String!
  voteCount: Int!
  author: User!
  detailList: [ApproachDetail!]!
}

type Need implement Node {
  id: ID!
  createdAt: String!
  content: String!
  tags: [String!]!
  author: User!
  approachList: [Approach!]!
}

union NeedOrApproach = Need | Approach

type Query {
  needFeaturedList: [Need!]!
  search(query: String!): [NeedOrApproach!]!
  needInfo(id: ID!): Need
  viewer(authToken: String!): User
}

input UserInput {
  email: String!
  password: String!
  firstName: String
  lastName: String
}

input AuthInput {
  email: String!
  password: String!
}

input ApproachDetailInput {
  content: String!
  category: ApproachCategory!
}

input ApproachInput {
  content: String!
  detailList: [ApproachDetailInput!]!
}

input NeedInput {
  content: String!
  approachList: [ApproachInput!]!
}

input ApproactVoteInput {
  """true for up-vote and false for down-vote"""
  up: Boolean!
}

type UserError {
  message: String!
  field: [String!]
}

type UserPayload {
  userErrors: [UserError!]!
  user: User
  currentAuthToken: String!
}

type NeedPayload {
  userErrors: [UserError!]!
  need: Need
}

type ApproachPayload {
  userErrors: [UserError!]!
  approach: Approach
}

type Mutation {
  userCreate(
    input: UserInput!
  ): UserPayload!

  userLogin(
    input: AuthInput!
  ): UserPayload!

  needCreate(
    authToken: String!
    input: NeedInput!
  ): NeedPayload!

  needUpdate(
    authToken: String!
    needId: ID!
    input: NeedInput!
  ): NeedPayload!

  approachCreate(
    authToken: String!
    needId: String!
    input: ApproachInput!
  ): ApproachPayload!

  approachVote(
    authToken: String!
    approachId: String!
    input: ApproachVoteInput!
  ): ApproachPayload!
}

type Subscription {
  needListActivity: [Need!]

  approachListActivity(
    needId: String!
  ): [Approach!]
}

Chapter 5

Listing 5.1 - lib/config.js
export const pgConfig = {
  database: process.env.PG_DATABASE,
};

export const mongoConfig = {
  database: process.env.MONGO_DATABASE,
  url: process.env.MONGO_URL,
};
Listing 5.2 - .env
export PG_DATABASE="YOUR_PG_DEV_DB_NAME_HERE"

export MONGO_DATABASE="azdev"
export MONGO_URL="mongodb://localhost:27017"
Listing 5.3 - lib/db-clients/pg.js
import pg from 'pg';
import { pgConfig } from '../config.js';

export default async () => {
  const pgPool = new pg.Pool(pgConfig);

  // Test the connection
  const client = await pgPool.connect();
  const tableCountResp = await client.query(
    'select count(*) from information_schema.tables where table_schema = $1;',
    ['azdev']
  );
  client.release();

  console.log(
    'Connected to PostgreSQL | Tables count:',
    tableCountResp.rows[0].count
  );

  pgPool.on('error', (err) => {
    console.error('Unexpected PG client error', err);
    process.exit(-1);
  });

  return {
    pgPool,
    pgClose: async () => await pgPool.end(),
  };
};
Listing 5.4 - lib/db-clients/mongo.js
import mdb from 'mongodb';
import { mongoConfig } from '../config.js';

export default async () => {
  const client = new mdb.MongoClient(mongoConfig.url, {
    useNewUrlParser: true,
  });
  try {
    await client.connect();
    const mdb = client.db(mongoConfig.database);

    // Test the connection
    const collections = await mdb.collections();
    console.log(
      'Connected to MongoDB | Collections count:',
      collections.length
    );

    return {
      mdb,
      mdbClose: () => client.close(),
    };
  } catch (err) {
    console.error('Error in MongoDB Client', err);
    process.exit(1);
  }
};
Listing 5.6 - In "psql": queries to read data
SELECT * FROM azdev.users;

SELECT * FROM azdev.needs;

SELECT * FROM azdev.approaches;
Listing 5.7 - In "mongo": command to read the approaches data
db.approachDetails.find({});
Listing 5.16 - lib/server.js
import { graphql } from 'graphql';
import { schema, rootValue } from './schema/index.js';

const executeGraphQLRequest = async request => {
  const resp = await graphql(schema, request, rootValue);

  console.log(resp.data);
};

executeGraphQLRequest(process.argv[2]);
Listing 5.17 - New content of lib/server.js
import express from 'express';
import graphqlHTTP from 'express-graphql';
import { schema, rootValue } from './schema/index.js';
Listing 5.18 - In lib/server.js
const server = express();

server.listen(8484, () => {
  console.log('API server is running');
});
Listing 5.20 - Mounting a GraphQL service under an HTTP route
server.use(
  '/',
  graphqlHTTP({
    schema,
    rootValue,
    graphiql: true,
  })
);

Chapter 6

Listing 6.1 - New file: lib/schema/types/need.js
import {
  GraphQLID,
  GraphQLObjectType,
  GraphQLString,
  GraphQLNonNull,
  GraphQLList,
} from 'graphql';

const Need = new GraphQLObjectType({
  name: 'Need',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    content: {
      type: new GraphQLNonNull(GraphQLString),
    },
    tags: {
      type: new GraphQLNonNull(
        new GraphQLList(new GraphQLNonNull(GraphQLString))
      ),
    },
    createdAt: {
      type: new GraphQLNonNull(GraphQLString),
    },
  },
});

export default Need;
Listing 6.6 - Changes in lib/server.js (in bold)
import express from 'express';
import graphqlHTTP from 'express-graphql';
import { schema } from './schema/index.js';

import pgClient from './db-clients/pg.js';

const startGraphQLWebServer = async () => {
  const { pgPool } = await pgClient();

  const server = express();
  server.use(
    '/',
    graphqlHTTP({
      schema,
      context: { pgPool },
      graphiql: true,
    })
  );

  server.listen(8484, () => {
    console.log('API server is running');
  });
};

startGraphQLWebServer();
Listing 6.10 - Changes in lib/schema/index.js (in bold)
import {
  // ...
  GraphQLList,
} from 'graphql';

import Need from './types/need.js';

// ...

const QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    // ...
    needFeaturedList: {
      type: new GraphQLNonNull(
        new GraphQLList(new GraphQLNonNull(Need))
      ),
      resolve: async (source, args, { pgPool }) => {
        const pgResp = await pgPool.query(`
          SELECT * FROM azdev.needs WHERE is_featured = true
        `);
        return pgResp.rows;
      },
    },
  },
});

// ...
Listing 6.16 - New file: lib/db-api/pg.js
import pgClient from '../db-clients/pg.js';

const sqls = {
  featuredNeeds:
    'SELECT * FROM azdev.needs WHERE is_featured = true;',
};

const pgApiWrapper = async () => {
  const { pgPool } = await pgClient();

  const pgQuery = (text, params) => pgPool.query(text, params);

  return {
    needs: {
      allFeatured: async () => {
        const pgResp = await pgQuery(sqls.featuredNeeds);
        return pgResp.rows;
      },
    },
  };
};

export default pgApiWrapper;
Listing 6.17 - Changes in lib/server.js (in bold)
import pgApiWrapper from './db-api/pg.js';

const server = express();

const startGraphQLWebServer = async () => {
  const pgApi = await pgApiWrapper();

  server.use(
    '/',
    graphqlHTTP({
      schema,
      context: { pgApi },
      graphiql: true,
    })
  );
Listing 6.18 - Changes in lib/schema/index.js (in bold)
needFeaturedList: {
  type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Need))),
  resolve: async (source, args, { pgApi }) => {
    return pgApi.needs.allFeatured();
  },
},
Listing 6.19 - The needFeaturedList complete query
{
  needFeaturedList {
    id
    content
    tags
    createdAt

    author {
      id
      email
      name
      createdAt
    }

    approachList {
      id
      content
      createdAt

      author {
        id
        email
        name
        createdAt
      }
    }
  }
}
Listing 6.20 - In lib/db-api/pg.js: SQL statement to get information about a single user
const sqls = {
  // ...
  userInfo: 'SELECT * FROM azdev.users WHERE id = $1',
};
Listing 6.21 - In lib/db-api/pg.sql: New function to use the userInfo SQL statement
const pgApiWrapper = async () => {
  // ...
  return {
    // ...
    users: {
      byId: async (userId) => {
        const pgResp = await pgQuery(sqls.userInfo, [userId]);
        return pgResp.rows[0];
      },
    },
  };
};
Listing 6.23 - New file: lib/schema/types/user.js
import {
  GraphQLID,
  GraphQLObjectType,
  GraphQLString,
  GraphQLNonNull,
} from 'graphql';

const User = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    email: {
      type: new GraphQLNonNull(GraphQLString),
    },
    name: {
      type: GraphQLString,
      resolve: ({ first_name, last_name }) => `${first_name} ${last_name}`,
    },
    createdAt: {
      type: new GraphQLNonNull(GraphQLString),
      resolve: ({ created_at }) => created_at.toISOString(),
    },
  },
});

export default User;
Listing 6.24 - Changes in lib/schema/types/need.js (in bold)
import User from './user.js';

const Need = new GraphQLObjectType({
  name: 'Need',
  fields: {
    // ...

    author: {
      type: new GraphQLNonNull(User),
      resolve: (source, args, { pgApi }) =>
        pgApi.users.byId(source.user_id),
    },
  },
});
Listing 6.26 - Changes to the name field in lib/schema/types/user.js
name: {
  type: new GraphQLNonNull(GraphQLString),
  resolve: ({ first_name, last_name }) =>
    [first_name, last_name].filter(Boolean).join(' '),
},
Listing 6.28 - In lib/db-api/pg.js | JOIN SQL statement (in bold)
const views = {
  needsAndUsers: `
    SELECT n.*,
      u.id AS author_id,
      u.email AS author_email,
      u.first_name AS author_first_name,
      u.last_name AS author_last_name,
      u.created_at AS author_created_at
    FROM azdev.needs n
    JOIN azdev.users u ON (n.user_id = u.id)
  `,
};
Listing 6.29 - Using a join view in SQL
const sqls = {
  featuredNeeds: `
    SELECT *
    FROM (${views.needsAndUsers}) nau
    WHERE is_featured = true;
  `,
};
Listing 6.30 - Change the resolve function for the Need.author field
import { extractPrefixedColumns } from '../utils.js';

const Need = new GraphQLObjectType({
  name: 'Need',
  fields: {
    // ...

    author: {
      type: new GraphQLNonNull(User),
      resolve: prefixedObject =>
        extractPrefixedColumns({ prefixedObject, prefix: 'author' }),
    },
  },
});
Listing 6.31 - New function in lib/schema/utils.js
export const extractPrefixedColumns = ({ prefixedObject, prefix }) => {
  const prefixRexp = new RegExp(`^${prefix}_(.*)`);
  return Object.entries(prefixedObject).reduce((acc, [key, value]) => {
    const match = key.match(prefixRexp);
    if (match) {
      acc[match[1]] = value;
    }
    return acc;
  }, {});
};
Listing 6.33 - Changes in lib/schema/types/need.js (in bold)
import Approach from './approach.js';

const Need = new GraphQLObjectType({
  name: 'Need',
  fields: {
    // ...
    approachList: {
      type: new GraphQLNonNull(
        new GraphQLList(new GraphQLNonNull(Approach))
      ),
      resolve: (source, args, { pgApi }) =>
        pgApi.needs.approachList(source.id),
    },
  },
});
Listing 6.35 - New file in lib/schema/types/approach.js
import {
  GraphQLID,
  GraphQLObjectType,
  GraphQLString,
  GraphQLNonNull,
} from 'graphql';

import User from './user.js';

const Approach = new GraphQLObjectType({
  name: 'Approach',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    content: {
      type: new GraphQLNonNull(GraphQLString),
    },
    createdAt: {
      type: new GraphQLNonNull(GraphQLString),
      resolve: ({ created_at }) => created_at.toISOString(),
    },

    author: {
      type: new GraphQLNonNull(User),
      resolve: (source, args, { pgApi }) =>
        pgApi.users.byId(source.user_id),
    },
  },
});

export default Approach;
Listing 6.36 - Changes in lib/dib-api/pgs.js: New SQL statement (in bold)
const sqls = {
  // ...
  approachesForNeed:
    'SELECT * FROM azdev.approaches WHERE need_id = $1',
};
Listing 6.37 - Changes in lib/db-api/pg.js (in bold)
const pgApiWrapper = async () => {
  // ...
  return {
    needs: {
      // ...
      approachList: async (needId) => {
        const pgResp = await pgQuery(sqls.approachesForNeed, [
          needId,
        ]);
        return pgResp.rows;
      },
    },
    // ...
  };
};

Chapter 7

Part 3

Chapter 8

Chapter 9