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-gateway
TypeScript thin layer powered by Apollo Gatewaygraphql-todo-items
TypeScript Federated Graph responsible for Todo Items (and extending Lists)graphql-todo-lists
TypeScript Federated Graph responsible for Todo Listsmicroservice-todo-items
Kotlin Spring Microservice responsible for operations related to Todo Itemsmicroservice-todo-lists
Kotlin Spring Microservice responsible for operations related to Todo Listsweb
React 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 withafter
argument; 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 withbefore
argument; 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.