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

Made with Slides.com