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