How we switched to
Server side
Jindřich Máca
OUTLINE
-
View on GraphQL
-
What is it?
-
-
Demo application
-
Technologies
-
-
Learn by examples :)
-
How we use GraphQL in Qest
-
sli.do - #QEETUP8
What is GraphQL?
Brief introduction
-
GraphQL is a query language for API
-
Need to say it is a strongly typed language
-
How it works:
-
First you define schema for your data
-
Then you can ask custom API queries over it
-
This isn’t bright new idea, but rather reflection of needs arising from development
Schema example
Query example and result
type Query {
songs: [Song!]!
}
type Song {
id: ID!
name: String!
description: String!
...
}
query {
songs {
name
}
}
{
"data": {
"songs": [
{
"name": "Bad Dream Baby"
},
...
]
}
}
Comparison with REST
- Might be more efficient on data transfer
- Serves more needs of developers
- Ask for what you need
- Get many resources in single request
- File transfers in general
- Error handling over HTTP
-
Technology not much adopted by industry yet
- Existing libraries, tools, standards etc.
You can combine GraphQL with REST for handling files or utilizing existing REST APIs
Apollo Server
GraphQL provides language, but you need implementation for your programming language (library)
GraphQL MVC
*What is your view in REST?
Demo App
We'll show you GraphQL principles on our application about songs
Qestify
It's all about songs:
- list, detail, search
- play, like, comment
- tags, logo, artist
- and more...
DEMO time
Let's write some queries!
Simple Queries
Let's get our data!
Schema
scalar NonNegativeInt
type Query
# -----------------------------------
extend type Query {
songs: [Song!]!
search(name: String!): [Song!]!
}
# -----------------------------------
type Song {
id: ID!
name: String!
artist: String!
cover: String!
description: String!
listens: NonNegativeInt!
tags: [Tag!]!
audio: String
isLiked: Boolean
comments: [Comment!]
}
type Tag {
value: String!
isImportant: Boolean
}
type Comment {
user: User!
text: String!
}
# -----------------------------------
interface User {
name: String!
avatar: String!
isArtist: Boolean
}
# -----------------------------------
type RegularUser implements User {
name: String!
avatar: String!
isArtist: Boolean
}
type ArtistUser implements User {
name: String!
avatar: String!
isArtist: Boolean
}
Resolvers
export const queries: ResolverObject = {
songs: async (_, __, { dataSources: { songs } }) => {
return songs.getAll();
},
search: async (_, { name }: QuerySearchArgs, { dataSources: { songs } }) => {
return songs.searchByName(name);
},
};
Where do we get those types?
Generating TS types
from GraphQL
Type Resolvers
export const types: ResolverObject = {
User: {
__resolveType: (user: User) => (user.isArtist ? 'ArtistUser' : 'RegularUser'),
},
Song: {
comments: (song: Song, _, { dataSources: { songs } }) => songs.getComments(song.id),
},
};
Data Source
export class SongsDataSource extends InMemoryDataSource {
async getAll() {
return songs;
}
async searchByName(name: string) {
return songs.filter((song) => song.name.includes(name));
}
}
Mutations
Let's modify our data!
Schema
directive @songExists on FIELD_DEFINITION
type Mutation
# -----------------------------------
extend type Mutation {
setLike(songId: ID!, like: Toggle!): Song! @songExists
addComment(songId: ID!, comment: CommentInput!): Song! @songExists
}
# -----------------------------------
enum Toggle {
ADD
REMOVE
}
# -----------------------------------
input CommentInput {
user: UserInput!
text: String!
}
input UserInput {
name: String!
avatar: String!
isArtist: Boolean
}
Directives
export class SongExistenceDirective extends SchemaDirectiveVisitor {
private async validateSongExistence(songId: string, { dataSources: { songs } }: Context) {
if (!(await songs.exist(songId))) {
throw new UserInputError(
`Song with ID '${songId}' does not exists.`,
{ invalidSongId: songId },
);
}
}
visitFieldDefinition(field: GraphQLField<{ songId: string }>) {
const { resolve = defaultFieldResolver, subscribe } = field;
field.resolve = async (...args) => {
const [, { songId }, context] = args;
await this.validateSongExistence(songId, context);
return resolve.apply(field, args);
};
if (!subscribe) return;
field.subscribe = async (...args) => {
const [, { songId }, context] = args;
await this.validateSongExistence(songId, context);
return subscribe.apply(field, args);
};
}
}
Resolvers
export const mutations: ResolverObject = {
setLike: async (_, { songId, like }: MutationSetLikeArgs, { dataSources: { songs } }) => {
return songs.setLike(songId, like);
},
addComment: async (_, { songId, comment }: MutationAddCommentArgs, { dataSources: { songs } }) => {
pubsub.publish(Triggers.COMMENT_ADDED, { songId, commentAdded: comment });
return songs.addComment(songId, comment);
},
};
Data Source
export class SongsDataSource extends InMemoryDataSource {
private async getById(songId: string) {
return songs[songs.findIndex((song) => song.id === songId)];
}
async setLike(songId: string, like: Toggle) {
const song = await this.getById(songId);
song.isLiked = like === Toggle.ADD;
return song;
}
async addComment(songId: string, comment: CommentInput) {
if (!comments[songId]) comments[songId] = [];
comments[songId].push(comment);
return this.getById(songId);
}
}
Subscription
For realtime data!
Schema
type Subscription
# -----------------------------------
extend type Subscription {
commentAdded(songId: ID!): Comment! @songExists
listens(songId: ID!): NonNegativeInt! @songExists
}
Resolvers
export enum Triggers {
COMMENT_ADDED = 'COMMENT_ADDED',
LISTENER_ADDED = 'LISTENER_ADDED',
}
export const subscriptions: ResolverObject = {
commentAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator<Comment>(Triggers.COMMENT_ADDED),
(payload: { songId: string }, { songId }: SubscriptionCommentAddedArgs) => {
return payload.songId === songId;
},
),
},
listens: {
subscribe: withFilter(
(_, { songId }: SubscriptionListensArgs, { dataSources: { songs } }: Context) => {
const iterator = pubsub.asyncIterator<number>(Triggers.LISTENER_ADDED);
songs.addListener(songId).then((song) => pubsub.publish(
Triggers.LISTENER_ADDED,
{ songId, listens: song.listens },
));
return iterator;
},
(payload: { songId: string }, { songId }: SubscriptionListensArgs) => {
return payload.songId === songId;
},
),
},
};
Data Source
export class SongsDataSource extends InMemoryDataSource {
async addListener(songId: string) {
const song = await this.getById(songId);
song.listens++;
return song;
}
}
Integration Testing
DEMO time
Let's run some tests!
Tools for testing
Apollo Server Testing
Jest
Examples
describe('Songs Queries', () => {
it('should return all songs', async () => {
expect.assertions(1);
const { query } = createApolloTestClient();
const { data: songs } = await query({ query: ALL_SONGS });
expect(songs).toMatchSnapshot();
});
it('shloud find all songs with given name', async () => {
expect.assertions(1);
const { query } = createApolloTestClient();
const { data: songs } = await query({
query: SEARCH_SONGS,
variables: { name: 'Bad' },
});
expect(songs).toMatchSnapshot();
});
});
const ALL_SONGS = gql`
query AllSongs {
songs {
name
artist
cover
description
listens
tags {
value
isImportant
}
audio
isLiked
comments {
user {
name
avatar
isArtist
}
text
}
}
}
`;
Thank You!
sli.do - #QEETUP8
How we switched to GraphQL - Server
By Jindřich Máca
How we switched to GraphQL - Server
- 906