Overview of a more realistic Example
In this section we will look at a setup that's typical to our clients.
This repository - https://github.com/xolvio/chimp-gql-federation-example consists of 6 independent packages:
graphql-gatewayTypeScript thin layer powered by Apollo Gatewaygraphql-todo-itemsTypeScript Federated Graph responsible for Todo Items (and extending Lists)graphql-todo-listsTypeScript Federated Graph responsible for Todo Listsmicroservice-todo-itemsKotlin Spring Microservice responsible for operations related to Todo Itemsmicroservice-todo-listsKotlin Spring Microservice responsible for operations related to Todo ListswebReact Frontend - a playground frontend that works with this architecture even though it was written over multiple years for a monolithic GraphQL / MongoDB server - but since the GraphQL Schema matches our Federated, it works just fine.
Setup
npm install
Make sure you have docker running. Then start all the projects including a dockerized instance of mongodb.
npm start
Development
Open graphql-todo-lists or graphql-todo-items
To run tests:
npm test
To run tests in watch mode:
npm run test:watch
To run type check:
npm run type-check
To run lint:
npm run lint
Common functionality
Our example repo shows implementation examples for common functionalities - Pagination, Filtering, Access Permissions, Caching.
If your project needs any of those things, feel free to reuse what you see, but take note that our implementation is in every case completely decoupled from the tooling we are introducing here. Meaning - you could use the same patterns in any JavaScript based GraphQL setup, also - that you could use any pattern that you find (or come up with) with our generator.
Working with pagination
Relevant files:
Pagination allows to narrow query results to a specific subset that satisfies pagination arguments passed in the query.
The implementation of pagination mechanism in this repo is based on Relay standard of pagination, which is described at https://relay.dev/graphql/connections.htm
There are four pagination arguments that can be passed to a query
after: a cursor (ID) of an item that is the left boundary of returned subset of items (if not passed then there is no left boundary)before: a cursor (ID) of an item that is the right boundary of returned subset of items (if not passed then there is no right boundary)first: a non-negative integer, should be passed either on its own or along withafterargument; defines amount of items to be returned that are first items foundlast: a non-negative integer, should be passed either on its own or along withbeforeargument; defines amount of items to be returned that are last items found
More detailed description of pagination arguments can be found at https://relay.dev/graphql/connections.htm#sec-Arguments
Results returned by pagination query consist not only of items that would be expected to be the result of the query.
Each paginated query returns a connection, that stores result array of items as edges array, where edge is an object consisting of node (item itself) and cursor (usually ID of an item).
Additionally, connection also contains pageInfo which provides information about start/end cursors of returned edges and whether there are previous and next pages.
Example:
In short, edges contain data for which query was built, and pageInfo consists of information necessary to ensure that paginated query can be wired up to some sort of frontend solution.
More information on format of returned data can be found at https://relay.dev/graphql/connections.htm#sec-Connection-Types
Working with filtering
Relevant files:
Another mechanism that allows narrowing of result number is filtering, by returning only items that are satisfying predicates constructed based on query arguments.
Example:
In example above, there is an extra filter argument added to the lists Query.
It consists of optional string field partialName, which can be used on the backend to construct a regular expression-based predicate,
that will filter out any List items that do not have partialName string as a part of their name.
It is important to note that GraphQL itself does not do filtering, so any filtering mechanism has to be implemented as a part of resolver/context/use case.
Enforcing access permissions
A custom directive called @authorized has been added to the schema. It allows annotations on both the FIELD_DEFINITION and the OBJECT level. The @authorized directive also supports multiple roles to be passed as a part of a given directive.
In the example below, the TodoItem type will only be resolved if the user has either the REGISTERED_USER or ADMIN role. Given a user that has the REGISTERED_USER role, they will be able to resolve all of the TodoItem fields except the list field.
Example:
You can also specify to the Query and the Mutation GQL Types (yes, these are types in GQL), so one can annotate them as such:
Roles which are required in the @authorized directive should be defined in the source schema.graphql file located at ./<federated-service>/src/graphql/ as an enum:
User role(s) are extracted from a JWT token sent with the request in the jwt cookie. See the AuthDirective.ts for the implementation of the directive, and the AuthModule.ts for unpacking the JWT. Each federated service which implements the @authorized directive expects the JWT shape to be like this:
If the incoming request does not have JWT cookie then it will be considered a user with no roles.
For development, use npm start from the root of the project. This will also run the auth-module ExpressJS server which has three routes defined:
http://localhost:4003/admin - Sets a cookie header with the Admin role in the JWT.
http://localhost:4003/user - Sets a cookie header with the REGISTERED_USER role in the JWT.
http://localhost:4003/logout - Removes the JWT cookie from the browser.