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 new idea, but rather reflection of needs arising from development (Facebook)
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?
Showcase of GraphQL principles on prototype application about songs
Qestify
It's all about songs:
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
}
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?
export const types: ResolverObject = {
Song: {
comments: (song: Song, _, { dataSources: { songs } }) => songs.getComments(song.id),
},
User: {
__resolveType: (user: User) => (user.isArtist ? 'ArtistUser' : 'RegularUser'),
},
};
Whatever you want!
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
}
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);
},
};
directive @songExists on FIELD_DEFINITION
# ---------------------------------------------------------
type Subscription
# -----------------------------------
extend type Subscription {
commentAdded(songId: ID!): Comment! @songExists
listens(songId: ID!): NonNegativeInt! @songExists
}
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>;
}
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;
},
),
},
};
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
}
}
}
`;
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}
`;