Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graphql example #93

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"awilix": "^4.3.4",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-graphql": "^0.12.0",
"graphql": "^15.6.1",
"joi": "^17.4.1",
"lodash.template": "^4.5.0",
"mongodb": "^4.0.0",
Expand Down
47 changes: 47 additions & 0 deletions src/_boot/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { asValue } from 'awilix';
import { graphqlHTTP } from 'express-graphql';
import { GraphQLObjectType, GraphQLSchema } from 'graphql';

import { makeSchemaStorage, RegisterSchema } from '@/_lib/graphql/schema';
import { makeModule } from '@/context';
import { config } from '@/config';
Beigelman marked this conversation as resolved.
Show resolved Hide resolved

const graphql = makeModule('graphql', async ({ app: { onBooted }, container: { build, register, cradle } }) => {
const { getSchemaData, registerSchema } = build(makeSchemaStorage);
Beigelman marked this conversation as resolved.
Show resolved Hide resolved

onBooted(async () => {
const { queries } = getSchemaData();

build(({ server }) => {
server.use(
'/graphql',
graphqlHTTP({
schema: new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
description: 'The root of all queries',
fields: () => ({
...queries,
}),
}),
}),
graphiql: config.environment !== 'production',
context: {
container: cradle,
Beigelman marked this conversation as resolved.
Show resolved Hide resolved
},
})
);
});
}, 'prepend');

register({
registerSchema: asValue(registerSchema),
});
});

type GraphQLRegistry = {
registerSchema: RegisterSchema;
};

export { graphql };
export type { GraphQLRegistry };
3 changes: 2 additions & 1 deletion src/_boot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Logger } from 'pino';
import { pubSub } from '@/_boot/pubSub';
import { MessageBundle } from '@/messages';
import { swagger } from '@/_boot/swagger';
import { graphql } from '@/_boot/graphql';

const main = withContext(async ({ app, container, config, bootstrap, logger, messageBundle }) => {
container.register({
Expand All @@ -19,7 +20,7 @@ const main = withContext(async ({ app, container, config, bootstrap, logger, mes
config: asValue(config),
});

await bootstrap(database, server, swagger, pubSub, repl, ...appModules);
await bootstrap(database, server, graphql, swagger, pubSub, repl, ...appModules);
});

type MainRegistry = {
Expand Down
32 changes: 32 additions & 0 deletions src/_lib/graphql/Graphql.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Registry } from '@/container';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid importing app-specific code into _lib code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see another way to do this at the moment.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could move the type GraphQLContext to inside the module file since it's app-specific.

import { GraphQLFieldConfig, GraphQLFieldResolver } from 'graphql';

type GraphQLContext = {
container: Registry;
};

type Sort = Readonly<{
field: string;
direction: 'asc' | 'desc';
}>;

type Pagination = Readonly<{
page: number;
pageSize: number;
}>;

type Filter = Record<string, any>;

type Args = {
pagination: Pagination;
filter: Filter;
sort: Sort[];
}

type GraphQLResolver = GraphQLFieldResolver<any, GraphQLContext, Args>

type GraphQLQueryMap<TSource, TContext> = {
[key: string]: GraphQLFieldConfig<TSource, TContext, Args>;
}

export { GraphQLContext, GraphQLQueryMap, GraphQLResolver };
37 changes: 37 additions & 0 deletions src/_lib/graphql/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
type SchemaData = {
queries: any;
mutations: any;
};
Beigelman marked this conversation as resolved.
Show resolved Hide resolved

type RegisterSchema = (schemaData: SchemaData) => void
type GetSchemaData = () => SchemaData;

type MakeSchemaStorage = {
getSchemaData: GetSchemaData;
registerSchema: RegisterSchema;
}

const makeSchemaStorage = (): MakeSchemaStorage => {
let queries = {};
let mutations = {};

return {
getSchemaData: () => ({ queries, mutations }),
Beigelman marked this conversation as resolved.
Show resolved Hide resolved
registerSchema: (schemaData: SchemaData): void => {
Beigelman marked this conversation as resolved.
Show resolved Hide resolved
queries = { ...queries, ...schemaData.queries };
mutations = { ...mutations, ...schemaData.mutations };
},
};
};

type Dependencies = {
registerSchema: RegisterSchema;
};

const withSchemaRegister =
(schemaData: SchemaData) =>
({ registerSchema }: Dependencies): void =>
registerSchema(schemaData);

export { makeSchemaStorage, withSchemaRegister };
Beigelman marked this conversation as resolved.
Show resolved Hide resolved
export type { RegisterSchema }
41 changes: 41 additions & 0 deletions src/_sharedKernel/interface/graphql/TypeDefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which kind of typing are we gonna have in this file? I think we can improve this name 🤔

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All generic types shared across the domains would be placed here

GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInt,
GraphQLList,
GraphQLString,
} from "graphql";
Beigelman marked this conversation as resolved.
Show resolved Hide resolved

const PaginationType = new GraphQLInputObjectType({
name: 'Pagination',
fields: () => ({
page: {
type: GraphQLInt,
defaultValue: 1
},
pageSize: {
type: GraphQLInt,
defaultValue: 10
}
})
})

const SortType = GraphQLList(new GraphQLInputObjectType({
name: 'Sort',
fields: () => ({
field: {
type: GraphQLString,
},
direction: {
type: new GraphQLEnumType({
name: 'Direction',
values: {
asc: { value: 'asc' },
desc: { value: 'desc' },
}
})
}
})
}))

export { PaginationType, SortType }
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { randomBytes } from 'crypto';
import { makeTestControls, TestControls } from '@/__tests__/TestControls';

describe('ArticleResolver', () => {
let controls: TestControls;

beforeAll(async () => {
controls = await makeTestControls();
});

// beforeEach(async () => {
// const { clearDatabase } = controls;

// await clearDatabase();
// });

// afterAll(async () => {
// const { cleanUp } = controls;

// await cleanUp();
// });
Beigelman marked this conversation as resolved.
Show resolved Hide resolved

describe('QUERY Articles', () => {
it('gets the first page of articles', async () => {
const { request } = controls;

const title = randomBytes(20).toString('hex');
const content = 'New Article content';
const getArticlesQuery = `
query Articles($filter: ArticleFilter, $sort: [Sort], $pagination: Pagination) {
articles(filter: $filter, sort: $sort, pagination: $pagination) {
id
title
comments {
id
body
createdAt
}
}
}
`;
const variables = {
pagination: {
page: 1,
pageSize: 10,
},
};

const response = await request().post('/api/articles').send({
title,
content,
});

await request().patch(`/api/articles/${response.body.id}/publish`);
Beigelman marked this conversation as resolved.
Show resolved Hide resolved

return request()
.post('/graphql')
.send({
operationName: 'Articles',
query: getArticlesQuery,
variables,
})
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding some abstraction to make it easier to send GraphQL calls in a test?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be interesting. Do you have something in mind?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the implementation per se in mind, but we could have something that would be used like this:

  return query('Articles', queryArticlesQuery, variables)
    .expect(async (res) => {
      // ...
    })

And something similar to mutations as well. What do you think?

.expect(async (res) => {
console.log(res);
expect(res.status).toBe(200);
const { data } = res.body;
expect(data).toHaveProperty('articles');
expect(data.articles).toHaveLength(1);
});
});
});
});
9 changes: 9 additions & 0 deletions src/article/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { makeMongoFindArticles } from '@/article/query/impl/MongoFindArticles';
import { makeModule } from '@/context';
import { makeArticleCreatedEmailListener } from '@/article/interface/email/ArticleCreatedEmailListener';
import { articleMessages } from '@/article/messages';
import { withSchemaRegister } from '@/_lib/graphql/schema';
import { articleQueries } from './interface/graphql';

const articleModule = makeModule(
'article',
Expand All @@ -34,6 +36,13 @@ const articleModule = makeModule(
findArticles: asFunction(makeMongoFindArticles),
});

build(
withSchemaRegister({
queries: articleQueries,
mutations: {},
Beigelman marked this conversation as resolved.
Show resolved Hide resolved
})
);

build(makeArticleController);
build(makeArticleCreatedEmailListener);
}
Expand Down
12 changes: 12 additions & 0 deletions src/article/interface/graphql/ArticleResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GraphQLResolver } from '@/_lib/graphql/Graphql';

const articleResolver: GraphQLResolver = async (_, args, context) => {
const { findArticles } = context.container;
const { filter, pagination, sort } = args;

const { data: articles } = await findArticles({ filter, pagination, sort });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should add some way to pass to the query which attributes we want according to the GraphQL query?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood your question correctly I think this is up to the client to deal with/decide

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is (and it's what is already happening), but we should also avoid fetching from the database the attributes the client does not want, right?


return articles;
};

export { articleResolver };
69 changes: 69 additions & 0 deletions src/article/interface/graphql/ArticleTypeDef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLObjectTypeConfig,
GraphQLString,
} from 'graphql';

import { GraphQLContext } from '@/_lib/graphql/Graphql';
import { ArticleListItemDTO } from '@/article/query/FindArticles';

type ArticlesConfigType = GraphQLObjectTypeConfig<ArticleListItemDTO, GraphQLContext>;

const CommentType = new GraphQLObjectType({
name: 'Comment',
fields: () => ({
id: {
type: GraphQLString,
description: 'The comment Id',
resolve: comment => comment.id,
},
body: {
type: GraphQLString,
description: 'The comment body',
resolve: comment => comment.body,
},
createdAt: {
type: GraphQLString,
description: 'The date the comment was wrote',
resolve: comment => comment.createAt
},
})
})

const ArticleTypeConfig: ArticlesConfigType = {
name: 'Article',
description: 'A blog post article',
fields: () => ({
id: {
type: GraphQLNonNull(GraphQLString),
description: 'The page Id',
resolve: article => article.id,
},
title: {
type: GraphQLString,
description: 'The article title',
resolve: article => article.title,
},
content: {
type: GraphQLString,
description: 'The article content',
resolve: article => article.content,
},
publishedAt: {
type: GraphQLString,
description: 'The date the article was published',
resolve: article => article.publishedAt
},
comments: {
type: GraphQLList(CommentType),
description: 'The article comments',
resolve: article => article.comments,
},
}),
};

const ArticleType = GraphQLList(new GraphQLObjectType(ArticleTypeConfig));

export { ArticleType };
Loading