Package Exports
- @ttoss/graphql-api
- @ttoss/graphql-api/shield
Readme
@ttoss/graphql-api
This package provides an opinionated way to create an GraphQL API using ttoss ecosystem modules. The main goal of this package is to provide a resilient way to create a complex GraphQL API to meet the following goals:
- Modular: you can create your GraphQL API using modules, so you can reduce the complexity of a big GraphQL API.
- Relay: ttoss uses Relay as the main GraphQL client, so this package implements the Relay Server Specification.
- Build Schema: as Relay needs an introspection query to work, this package provides a way to build the GraphQL schema by running
ttoss-graphl-api build-schema. - Build TypeScript Types: this package provides a way to build the TypeScript types for your GraphQL schema by running
ttoss-graphl-api build-schema. - AppSync Support: this package provides a way to create a GraphQL API that works with AWS AppSync, besides you can also create a local GraphQL API server.
Installation
pnpm add @ttoss/graphql-api graphqlQuickstart
This library uses graphql-compose to create the GraphQL schema. It re-export all the graphql-compose types and methods, so you can use it directly from this package.
Type Creation
For more examples about how to create types, check the graphql-compose documentation.
import { schemaComposer } from '@ttoss/graphql-api';
const UserTC = schemaComposer.createObjectTC({
name: 'User',
fields: {
id: 'ID!',
name: 'String!',
},
});Resolvers
Integrate All Modules
Once you've created all your types and resolvers, you can integrate all the modules to create the GraphQL schema.
// scr/schemaComposer.ts
import { schemaComposer } from '@ttoss/graphql-api';
import './modules/Module1/composer';
import './modules/Module3/composer';
import './modules/User/composer';
export { schemaComposer };Relay Server Specification
As ttoss uses Relay as the main GraphQL client, this library implements the Relay Server Specification.
Object Identification
Method composeWithRelay will handle the object identification for your ObjectTypeComposer, it will return a globally unique ID among all types in the following format base64(TypeName + ':' + recordId).
Method composeWithRelay only works if ObjectTypeComposer meets the following requirements:
Has defined
recordIdFn: returns the id for the globalId construction. For example, if you use DynamoDB, you could create id from hash and range keys:UserTC.setRecordIdFn((source) => { return `${source.hashKey}:${source.rangeKey}`; });
Have
findByIdresolver: this resolver will be used byRootQuery.nodeto resolve the object by globalId. For example:UserTC.addResolver({ name: 'findById', type: UserTC, args: { id: 'String!', }, resolve: ({ args }) => { const { type, recordId } = fromGlobalId(args.id); // find object }, });
Example
import {
composeWithRelay,
schemaComposer,
fromGlobalId,
} from '@ttoss/graphql-api';
const UserTC = schemaComposer.createObjectTC({
name: 'User',
fields: {
id: 'ID!',
name: 'String!',
},
});
/**
* 1. Returns you id for the globalId construction.
*/
UserTC.setRecordIdFn((source) => {
/**
* If you use DynamoDB, you could create id from hash and range keys:
* return `${source.hashKey}:${source.rangeKey}`;
*/
return source.id;
});
/**
* 2. Define `findById` resolver (that will be used by `RootQuery.node`).
*/
UserTC.addResolver({
name: 'findById',
type: UserTC,
args: {
id: 'String!',
},
resolve: ({ args }) => {
const { type, recordId } = fromGlobalId(args.id);
// find object
},
});
/**
* 3. This will add the `id` field and the `node` query.
*/
composeWithRelay(UserTC);We inspired ourselves on graphql-compose-relay to create composeWithRelay.
Connections
This packages provides the method composeWithConnection to create a connection type and queries for a given type, based on graphql-compose-connection plugin and following the Relay Connection Specification.
import { composeWithConnection } from '@ttoss/appsync-api';
AuthorTC.addResolver({
name: 'findMany',
type: AuthorTC,
resolve: async ({ args }) => {
// find many
},
});
composeWithConnection(AuthorTC, {
findManyResolver: AuthorTC.getResolver('findMany'),
countResolver: AuthorTC.getResolver('count'),
sort: {
ASC: {
value: {
scanIndexForward: true,
},
cursorFields: ['id'],
beforeCursorQuery: (rawQuery, cursorData, resolveParams) => {
if (!rawQuery.id) rawQuery.id = {};
rawQuery.id.$lt = cursorData.id;
},
afterCursorQuery: (rawQuery, cursorData, resolveParams) => {
if (!rawQuery.id) rawQuery.id = {};
rawQuery.id.$gt = cursorData.id;
},
},
DESC: {
value: {
scanIndexForward: false,
},
cursorFields: ['id'],
beforeCursorQuery: (rawQuery, cursorData, resolveParams) => {
if (!rawQuery.id) rawQuery.id = {};
rawQuery.id.$gt = cursorData.id;
},
afterCursorQuery: (rawQuery, cursorData, resolveParams) => {
if (!rawQuery.id) rawQuery.id = {};
rawQuery.id.$lt = cursorData.id;
},
},
},
});
schemaComposer.Query.addFields({
authors: Authors.getResolver('connection'),
});When you composeWithConnection a type, it will add the resolver connection to the type, so you can add to Query or any other type:
schemaComposer.Query.addFields({
authors: Authors.getResolver('connection'),
});The resolver connection has the following arguments based on the Relay Connection Specification:
first: the number of nodes to return.after: the cursor to start the query.last: the number of nodes to return.before: the cursor to start the query.sort: the sort option to use. It's the keys of thesortobject. In our example, it'sASCandDESC.filter: the filter to use. It'll exist if you add thefiltertofindManyResolverfor example, the implementation below will add thefilterargument with thenameandbookfields:AuthorTC.addResolver({ name: 'findMany', type: AuthorTC, args: { filter: { name: 'String', book: 'String', }, }, resolve: async ({ args }) => { // find many }, });
To configure composeWithConnection, you need to provide the following options:
findManyResolver
The resolver that will be used to find the nodes. It receives the following arguments:
args: theargsobject from the resolver. Example:AuthorTC.addResolver({ name: 'findMany', type: AuthorTC, args: { filter: { name: 'String', book: 'String', }, }, resolve: async ({ args, }: { args: { first?: number; after?: string; last?: number; before?: string; /** * The `filter` argument, if provided on the query. */ filter: { name: string; book: string; }; /** * The `sort` argument, if provided on the query as * they keys of the `sort` object. In our example * above, it's `ASC` and `DESC`. `scanIndexForward` * is the value of the `value` property on the sort * object. In our example above, it's `true` for * `ASC` and `false` for `DESC`. */ sort: { scanIndexForward: boolean; }; }; }) => { // }, });
rawQuery: an object created bybeforeCursorQueryorafterCursorQuerymethods from sort option.
countResolver
sort
It's an object that defines the sort options. Each key is the sort name and the value is an object with the following properties:
value: and object that the resolver will receive as thesortargument.cursorFields: an array of fields that will be used to create the cursor.beforeCursorQueryandafterCursorQuery: methods that will be used to create therawQueryobject for thefindManyResolver. They receive the following arguments:rawQuery: therawQueryobject that will be used to find the nodes.cursorData: the data from the cursor define oncursorFields. For example, if you definecursorFieldsas['id', 'name'], thecursorDatawill an object with theidandnameproperties.resolveParams: theresolveParamsobject from the resolver. You can accessargs,contextandinfoand other GraphQL properties from this object.
Example:
composeWithConnection(AuthorTC, { // ... sort: { ASC: { // ... cursorFields: ['id', 'name'], // Called when `before` cursor is provided. beforeCursorQuery: (rawQuery, cursorData, resolveParams) => { if (!rawQuery.id) rawQuery.id = {}; rawQuery.id.$lt = cursorData.id; rawQuery.name.$lt = cursorData.name; }, // Called when `after` cursor is provided. afterCursorQuery: (rawQuery, cursorData, resolveParams) => { if (!rawQuery.id) rawQuery.id = {}; rawQuery.id.$gt = cursorData.id; rawQuery.name.$gt = cursorData.name; }, }, }, });
In the example above, the
findManyResolverwill receive the followingrawQueryobject whenbeforecursor is provided:{ "id": { "$lt": "id-from-cursor" }, "name": { "$lt": "name-from-cursor" } }
Middlewares
This package provides a way to add middlewares to your final schema. You can add middlewares compatible with graphql-middleware by passing them to the middlewares option on buildSchema method. For example, you can use GraphQL Shield to add authorization to your API:
import { buildSchema } from '@ttoss/graphql-api';
import { allow, deny, shield } from '@ttoss/graphql-api/shield';
import { schemaComposer } from './schemaComposer';
const NotAuthorizedError = new Error('Not authorized!');
/**
* The error name is the same value `errorType` on GraphQL errors response.
*/
NotAuthorizedError.name = 'NotAuthorizedError';
const permissions = shield(
{
Query: {
'*': deny,
author: allow,
},
Author: {
id: allow,
name: allow,
},
},
{
fallbackRule: deny,
fallbackError: NotAuthorizedError,
}
);
const schema = buildSchema({
schemaComposer,
middlewares; [permissions],
})Shield
This package re-exports the all methods from GraphQL Shield.
import { allow, deny, shield } from '@ttoss/graphql-api/shield';Building Schema and Types
As Relay needs an introspection query to work, this package provides a way to build the GraphQL schema by running ttoss-graphl-api build-schema. It build the schema using the schemaComposer from src/schemaComposer.ts file and save the schema in schema/schema.graphql file and TypeScript types in schema/types.ts file.
ttoss-graphl-api build-schemaYou can add the build script to your package.json:
{
"scripts": {
"build": "ttoss-graphl-api build-schema"
}
}How to Create Tests
We recommend testing the whole GraphQL API using the graphql object and the schema composer to provide the schema. For example:
import { graphql } from 'graphql';
import { schemaComposer } from './schemaComposer';
test('testing my query', () => {
const author = {
id: '1',
name: 'John Doe',
};
const response = await graphql({
schema: schemaComposer.buildSchema(),
source: /* GraphQL */ `
query ($id: ID!) {
node(id: $id) {
id
... on Author {
name
}
}
}
`,
variableValues: {
id: author.id,
},
});
expect(response).toEqual({
data: {
node: {
id: author.id,
name: author.name,
},
},
});
});