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
- 396