Jindřich Máca
View on GraphQL
What is it?
Demo application
Technologies
Learn by examples :)
How we use GraphQL in Qest
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
type Query {
songs: [Song!]!
}
type Song {
id: ID!
name: String!
description: String!
...
}
query {
songs {
name
}
}
{
"data": {
"songs": [
{
"name": "Bad Dream Baby"
},
...
]
}
}
You can combine GraphQL with REST for handling files or utilizing existing REST APIs
GraphQL provides language, but you need implementation for your programming language (library)
*What is your view in REST?
We'll show you GraphQL principles on our application about songs
Qestify
It's all about songs:
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
}
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?
export const types: ResolverObject = {
User: {
__resolveType: (user: User) => (user.isArtist ? 'ArtistUser' : 'RegularUser'),
},
Song: {
comments: (song: Song, _, { dataSources: { songs } }) => songs.getComments(song.id),
},
};
export class SongsDataSource extends InMemoryDataSource {
async getAll() {
return songs;
}
async searchByName(name: string) {
return songs.filter((song) => song.name.includes(name));
}
}
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
}
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);
};
}
}
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);
},
};
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);
}
}
type Subscription
# -----------------------------------
extend type Subscription {
commentAdded(songId: ID!): Comment! @songExists
listens(songId: ID!): NonNegativeInt! @songExists
}
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;
},
),
},
};
export class SongsDataSource extends InMemoryDataSource {
async addListener(songId: string) {
const song = await this.getById(songId);
song.listens++;
return song;
}
}
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
}
}
}
`;