from
Jindřich Máca
with 💖
OUTLINE
-
View on GraphQL
-
What is it?
-
-
Demo application
-
Technologies
-
-
Learn by examples :)
-
How we use GraphQL in Qest
-
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 new idea, but rather reflection of needs arising from development (Facebook)
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
- Take one tour to a server for all data
- Serves more needs of developers / clients
- Ask for what you need
- Get many resources in single request
- File transfers in general
- Error handling over HTTP (not design to)
-
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
Showcase of GraphQL principles on prototype 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 {
song(id: ID!): Song!
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 = {
song: async (_, { id }: QuerySongArgs, { dataSources: { songs } }) => {
return songs.getById(id);
},
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 = {
Song: {
comments: (song: Song, _, { dataSources: { songs } }) => songs.getComments(song.id),
},
User: {
__resolveType: (user: User) => (user.isArtist ? 'ArtistUser' : 'RegularUser'),
},
};
Data Source
Whatever you want!
- Database
- REST API
- In memory
- ...
Mutations
Let's modify our data!
Schema
type Mutation
# -----------------------------------
extend type Mutation {
setLike(songId: ID!, like: Toggle!): Song!
addComment(songId: ID!, comment: CommentInput!): Song!
}
# -----------------------------------
enum Toggle {
ADD
REMOVE
}
# -----------------------------------
input CommentInput {
user: UserInput!
text: String!
}
input UserInput {
name: String!
avatar: String!
isArtist: Boolean
}
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);
},
};
Subscription
For realtime data!
Schema
directive @songExists on FIELD_DEFINITION
# ---------------------------------------------------------
type Subscription
# -----------------------------------
extend type Subscription {
commentAdded(songId: ID!): Comment! @songExists
listens(songId: ID!): NonNegativeInt! @songExists
}
Directives
type Args = { songId?: string };
export class SongExistenceDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: GraphQLField<Args>): void {
const { resolve = defaultFieldResolver, subscribe } = field;
field.resolve = applyDirective(field, resolve);
if (!subscribe) return;
field.subscribe = applyDirective(field, subscribe);
}
}
function applyDirective(field: GraphQLField<Args>, resolver: GraphQLFieldResolver<Args>) {
return (async (...resolverArgs) => {
const [, { songId }, context] = resolverArgs;
const {
dataSources: { songs },
} = context;
if (!songId) {
throw new ApolloError('Invalid use of SongExistenceDirective field must contain "songId" argument.');
}
await songs.getById(songId); // Test if a song exists
return resolver.apply(field, resolverArgs);
}) as GraphQLFieldResolver<Args>;
}
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;
},
),
},
};
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(2);
const { query } = createApolloTestClient();
const { data: songs, errors } = await query({ query: ALL_SONGS });
expect(errors).toBeFalsy();
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
}
}
}
`;
Fragments
const ALL_SONGS = gql`
query AllSongs {
songs {
name
artist
cover
description
listens
tags {
value
isImportant
}
audio
isLiked
comments {
user {
name
avatar
isArtist
}
text
}
}
}
`;
const SONG_FRAGMENT = gql`
fragment SongFragment on Song {
name
artist
cover
description
listens
tags {
value
isImportant
}
audio
isLiked
comments {
user {
name
avatar
isArtist
}
text
}
}
`;
const ALL_SONGS = gql`
query AllSongs {
songs {
...SongFragment
}
}
${SONG_FRAGMENT}
`;
It does not end here!
- Security
- Caching
- Persistent queries
- Federation
- Monitoring
- ...
Thank You!
GraphQL from Qest with Love
By Jindřich Máca
GraphQL from Qest with Love
- 507