GraphQL

In Deeper

API Structure

models - Database Model

processes - Behavior Functions

types - GraphQL Types

Controller Logic

Endpoint Define

DB Mixins

Bind queries and mutations

/* Import queries and mutations from scoped models */

import {
  orderQueries,
  orderMutations,
} from './Order.js';

/* Spread into root schema */

const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    ...orderQueries,
    /* ... other model queries */
  }),
});

const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: () => ({
    ...orderMutations,
    /* ... other models mutations */
  }),
});

export const graphqlSchema = new GraphQLSchema({
  query: queryType,
  mutation: mutationType,
});

Model Sample - 1

/* import query, mutation and helper functions in scoped process */
import {
  makeOrder,
  makeOrderMutation,
  getCheckoutToken,
} from '../processes/Order/makeOrder.js';
import {
  listOrdersQuery,
  findOrdersWithConditions,
  singleOrderQuery,
  findOrderWithConditions,
} from '../processes/Order/list.js';

/* Define Database Model (MongoDB) */

const OrderSchema = new Schema({
  clearId: {
    type: String,
    required: true,
    trim: true,
    maxlength: 14,
  },
  receiver: OrderReceiverSchema,
  buyer: OrderBuyerSchema,
  totalPrice: Number,
  paymentInfo: OrderPaymentInfoSchema,
  subOrders: [SubOrderSchema],
});

Model Sample - 2

/* Define class and instance methods */

OrderSchema.virtual('favoriteType').get(() => 'Product');

OrderSchema.static('makeOrder', makeOrder);
OrderSchema.static('findAllWithConditions', findOrdersWithConditions);
OrderSchema.static('findOneWithConditions', findOrderWithConditions);

OrderSchema.method('getCheckoutToken', getCheckoutToken);

/* Initialize model */

const Order = mongoose.model('Order', OrderSchema);

/* Export query and mutation endpoints */

export const orderQueries = {
  orders: listOrdersQuery,
  order: singleOrderQuery,
};

export const orderMutations = {
  makeOrder: makeOrderMutation,
};

Process - 1

/* Define class and instance methods to export */

export function findOrdersWithConditions({
  limit,
  after,
  status,
}: findOrdersWithConditionsArgsType) {
  const {
    Order,
  } = mongoose.models;

  const whereClause = {};

  if (after) {
    if (!mongoose.Types.ObjectId.isValid(after)) {
      return [];
    }

    whereClause._id = {
      $gt: after,
    };
  }

  if (status) {
    whereClause['subOrders.status'] = status;
  }

  return Order
    .find(whereClause)
    .limit(limit)
    .sort('_id');
}

Process - 2

/* Endpoint definition */

export const listOrdersQuery = {
  type: new GraphQLList(orderType),
  args: {
    limit: {
      type: GraphQLInt,
      defaultValue: 5,
    },
    after: {
      type: GraphQLString,
    },
    status: {
      type: GraphQLString,
    },
  },
  resolve: (n: null, args: findOrdersWithConditionsArgsType) =>
    mongoose.models.Order.findAllWithConditions(args),
};

export const singleOrderQuery = {
  type: orderType,
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLString),
    },
  },
  resolve: (n: null, args: findOrderWithConditionsArgsType) =>
    mongoose.models.Order.findOneWithConditions(args),
};

Resolve function

Endpoint resolver

resolve(parent, args, req?, context)

rootValue

orders

members

name

avatar

totalPrice

id

Endpoint resolver

resolve(parent, args, req?, context)

export const listOrdersQuery = {
  type: new GraphQLList(orderType),
  args: {
    limit: {
      type: GraphQLInt,
      defaultValue: 5,
    },
    after: {
      type: GraphQLString,
    },
    status: {
      type: GraphQLString,
    },
  },
  resolve: (n: null, args: findOrdersWithConditionsArgsType) =>
    mongoose.models.Order.findAllWithConditions(args),
};

Endpoint resolver

resolve(parent, args, req?, context)

export type GraphQLResolveInfo = {
  fieldName: string,
  fieldASTs: Array<Field>,
  returnType: GraphQLOutputType,
  parentType: GraphQLCompositeType,
  schema: GraphQLSchema,
  fragments: { [fragmentName: string]: FragmentDefinition },
  rootValue: any,
  operation: OperationDefinition,
  variableValues: { [variableName: string]: any },
}
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: {
    ...models,
  },
  graphiql: process.env.NODE_ENV !== 'production',
}));

Subscription

Client

Query

Mutation

Process

Process

Read

Update

Delete

Create

Backend Structure

Client

Query

Mutation

Process

Process

Read

Update

Delete

Create

Message Queue

Queue

enqueue

Client

Query

Mutation

Process

Process

Read

Update

Delete

Create

Subscription

Queue

enqueue

Subscription

Event Stream

Client

Server

Text

Keep-Connection

Push Event

EmitEmitter.emit(name, data);

Structure

Implement

class EventDistributor extends EventEmitter;

const distributor = new EventDistributor():

app.get('/updateStream', (req, res) => {
  let counter = 0;

  distributor.on(RECORDS_CHANGE, (data) => {
    counter += 1;

    res.write(`id: ${counter}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  });

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  res.write('\n');
});
const source = new EventSource(`${API_HOST}/updateStream`);

source.addEventListener('message', (e) => {
  if (!e.name || e.name !== RECORDS_CHANGE) return;

  try {
    const ranking = JSON.parse(e.data);

    this.setState({
      ranking,
    });
  } catch (ex) {
    console.error(ex);
  }
});

Server

Client

Event Source @ GraphQL

subscription MyDroidEvents {
  members(limit: 5) {
    tpe: __typename
    eventId

    ... on NameChanged {
      oldName
      newName
    }
  }
}
POST /graphql
...
HTTP/1.1 200 OK
Content-Type: text/event-stream
...

id: 6
data: {"data": {"members": {"tpe": "NameChanged", "eventId": 6, "oldName": "foo", "newName": "bar"}}}

id: 7
data: {"data": {"members": {"tpe": "FriendAdded", "eventId": 7}}}

id: 8
data: {"data": {"members": {"tpe": "NameChanged", "eventId": 8, "oldName": "bar", "newName": "baz"}}}

WebSocket

WebSocket @ GraphQL

import { PubSub, SubscriptionManager } from 'graphql-subscriptions';

const pubsub = new PubSub();

const subscriptionType = new GraphQLObject({
  name: "Subscription",
  fields: {
    members: {
      type: memberSubscriptionType,
    },
  },
});
 
const schema = new GraphQLSchama({
  subscription: subscriptionType
});

const subscriptionManager = new SubscriptionManager({
  schema,
  pubsub,
});

/* ... event emit */
pubsub.publish('members', {
  id: 123,
  oldName: 'Tony',
  newName: 'Jack',
});

WebSocket @ GraphQL

import {
  SubscriptionClient,
  addGraphQLSubscriptions,
} from 'subscriptions-transport-ws';
import ApolloClient, { createNetworkInterface } from 'apollo-client';

const networkInterface = createNetworkInterface({
  uri: `http://${API_HOST}/graphql`,
});

const webSocketInterface = new SubscriptionClient(`ws://${API_HOST}/`, {
  reconnect: true,
});

const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
  networkInterface,
  webSocketInterface,
);

const apolloClient = new ApolloClient({
  networkInterface: networkInterfaceWithSubscriptions
});

GraphQL - In Deeper

By Chia Yu Pai

GraphQL - In Deeper

  • 388