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
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 User implement Node {
  id: ID!
  createdAt: String!
  email: String
  name: String
  needList: [Need!]!
}

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!
  tags: [String!]!
  private: Boolean!
}

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

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

type UserPayload {
  errors: [UserError!]!
  user: User
  authToken: String
}

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

type ApproachPayload {
  errors: [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,
};

When it’s time to run this service in production, we’ll define different process environment variables. For local development, create an .env file somewhere and seed it with the following lines:

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,
    useUnifiedTopology: 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);
  }
};
SEED DATA - seed-data/index.js
const switchStatement1 = `switch (expression) {
  case value1:
    // do something when expression === value1
    break;
  case value2:
    // do something when expression === value2
    break;
  default:
    // do something when expression does not equal any of the values above
}`;

const switchStatement2 = `function doSomethingFor(expression) {
  switch (expression) {
    case value1:
      // do something when expression === value1
      return;
    case value2:
      // do something when expression === value2
      return;
    default:
      // do something when expression does not equal any of the values above
  }
}`;

const babelReactEnv = `module.exports = {
  presets: [
    ''@babel/react'',
    [
      ''@babel/env'',
      {
        modules: ''commonjs'',
        targets: [
          ''> 1%'',
          ''last 3 versions'',
          ''ie >= 9'',
          ''ios >= 8'',
          ''android >= 4.2'',
        ],
      },
    ],
  ],
  plugins: [
    [''@babel/plugin-proposal-class-properties'', {loose: true}],
  ],
};`;

export default {
  0: `
    INSERT INTO azdev.users (email, hashed_password, hashed_auth_token, is_admin)
    VALUES ('[email protected]', crypt('testPass123', gen_salt('bf')), crypt('testToken123', gen_salt('bf')), FALSE)
    RETURNING id;
  `,

  1: {
    need: `
      INSERT INTO azdev.needs (content, tags, user_id, is_public)
      VALUES ('Caluclate the sum of numbers in a JavaScript array', 'code,javascript', $1, TRUE)
      RETURNING id;`,
    approaches: [
      {
        pg: `
          INSERT INTO azdev.approaches (content, user_id, need_id, vote_count)
          VALUES ('arrayOfNumbers.reduce((acc, curr) => acc + curr, 0)', $1, $2, 7)
          RETURNING id;`,
        mongo: {
          explanations: [
            'The `reduce` method invokes its callback function (the first argument) on every item in `arrayOfNumbers`. Each invokation supplies the callback function with an "accumulator" argument and the "current" item for that invokation. What the callback function returns becomes the new value for the accumulator. The initial value of the accumulator is the second argument to `reduce`. By starting with 0 and always returning the sum of the accumulator and the current number in the array, the final result will be the sum of all numbers in `arrayOfNumbers`.',
          ],
        },
      },
    ],
  },

  2: {
    need: `
      INSERT INTO azdev.needs (content, tags, user_id, is_public, is_featured)
      VALUES ('Get rid of only the unstaged changes since the last git commit', 'command,git', $1, TRUE, TRUE)
      RETURNING id;`,
    approaches: [
      {
        pg: `
          INSERT INTO azdev.approaches (content, user_id, need_id)
          VALUES ('git diff | git apply --reverse', $1, $2)
          RETURNING id;`,
        mongo: {
          notes: [
            'This will work if you have staged changes (that you want to keep) or even untracked files. It will only git rid of the unstaged changes.',
          ],
        },
      },
    ],
  },

  3: {
    need: `
      INSERT INTO azdev.needs (content, tags, user_id, is_public, is_featured)
      VALUES ('The syntax for a switch statement (AKA case statement) in JavaScript', 'code,javascript', $1, TRUE, TRUE)
      RETURNING id;`,
    approaches: [
      {
        pg: `
          INSERT INTO azdev.approaches (content, user_id, need_id, vote_count)
          VALUES ('${switchStatement1}', $1, $2, 5)
          RETURNING id;`,
        mongo: {
          notes: [
            'The `break` statements are needed. Without them, JavaScript will continue to execute all the lines in all the other cases after the one that was matched. That is rarely the intended behaviour (altough you can use it to define multiple cases that are intended to execute the same code. For example, do something if `expression` equals either `value1` or `value2`)',
          ],
        },
      },
      {
        pg: `
          INSERT INTO azdev.approaches (content, user_id, need_id, vote_count)
          VALUES ('${switchStatement2}', $1, $2, 18)
          RETURNING id;`,
        mongo: {
          explanations: [
            'Because the function returns for each case, there is no need to "break" out of that case. You can make the function optionally return a value based on the expression as well.',
          ],
        },
      },
    ],
  },

  4: {
    need: `
      INSERT INTO azdev.needs (content, tags, user_id, is_public)
      VALUES ('Babel configuration file for "react" and "env" presets with a list of plugins', 'config,javascript,node', $1, TRUE)
      RETURNING id;`,
    approaches: [
      {
        pg: `
          INSERT INTO azdev.approaches (content, user_id, need_id)
          VALUES ('${babelReactEnv}', $1, $2)
          RETURNING id;`,
        mongo: {
          notes: [
            'This will only work for Babel versions > 7.x. Older Babels require a different configuration.',
          ],
        },
      },
    ],
  },

  5: {
    need: `
      INSERT INTO azdev.needs (content, tags, user_id, is_public, is_featured)
      VALUES ('Create a secure one-way hash for a text value (like a password) in Node', 'code,node', $1, TRUE, TRUE)
      RETURNING id;`,
    approaches: [
      {
        pg: `
          INSERT INTO azdev.approaches (content, user_id, need_id)
          VALUES ('const bcrypt = require(''bcrypt'');\nconst hashedPass = bcrypt.hashSync(''testPass123'', 10);', $1, $2)
          RETURNING id;`,
        mongo: {
          explanations: [
            'The second argument to hashSync (or hash) is for the "salt" to be used to hash the text. When specified as a number then a salt will be generated with the specified number of rounds and used.',
          ],
          notes: [
            'To do the hasing asynchronously, use the `bycrypt.hash` method. It returns a promise.',
            'To compare hashed texts together, bcrypt has a `compareSync` (and `compare`) methods',
          ],
        },
      },
    ],
  },
};
SEED DATA - seed-data/index.js
import pgClient from '../lib/db-clients/pg.js';
import mdbClient from '../lib/db-clients/mongo.js';
import data from './data.js';

const importAllData = async () => {
  console.info('Importing seed data');

  const { pgPool, pgClose } = await pgClient();
  const { mdb, mdbClose } = await mdbClient();

  /* The following "dangerous" 2 lines can be used to reset
     your database and delete all existing data.
     Make sure you're running them against a local databases! */
  await pgPool.query('TRUNCATE TABLE azdev.users CASCADE');
  await mdb.collection('approachDetails').deleteMany({});

  let pgResp;
  pgResp = await pgPool.query(data[0]);
  const userId = pgResp.rows[0].id;

  for (let i = 1; i <= 5; i++) {
    const { need, approaches } = data[i];
    pgResp = await pgPool.query(need, [userId]);
    const needId = pgResp.rows[0].id;

    for (const approach of approaches) {
      pgResp = await pgPool.query(approach.pg, [userId, needId]);
      const approachId = pgResp.rows[0].id;

      await mdb.collection('approachDetails').insertOne({
        pgId: approachId,
        ...approach.mongo,
      });
    }
  }

  console.info('Done importing seed data');
  pgClose();
  mdbClose();
};

importAllData();
Listing 5.7 - In "psql": queries to read data
SELECT * FROM azdev.users;

SELECT * FROM azdev.needs;

SELECT * FROM azdev.approaches;
Listing 5.8 - In "mongo": command to read the approaches data
db.approachDetails.find({});
Listing 5.17 - 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.18 - New content of lib/server.js
import express from 'express';
import graphqlHTTP from 'express-graphql';
import { schema, rootValue } from './schema/index.js';
Listing 5.19 - In lib/server.js
const server = express();

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

Chapter 6

Listing 6.4 - 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: 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/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,
  GraphQLInt,
  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),
    },
    voteCount: {
      type: new GraphQLNonNull(GraphQLInt),
      resolve: ({ vote_count }) => vote_count,
    },
    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

Chapter 8

Part 3

Chapter 9

Chapter 10

Chapter 11