-
Notifications
You must be signed in to change notification settings - Fork 545
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
base: main
Are you sure you want to change the base?
Graphql example #93
Changes from 9 commits
69a9aae
14c39e3
077ae5a
4e816d8
2962104
36f50f4
b6633d0
9475a33
1100292
a468bc0
139b3f5
4e1bf9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { asValue } from 'awilix'; | ||
import { graphqlHTTP } from 'express-graphql'; | ||
import { GraphQLObjectType, GraphQLSchema } from 'graphql'; | ||
|
||
import { makeSchema, AddToSchema } from '@/_lib/graphql/schema'; | ||
import { makeModule } from '@/context'; | ||
|
||
const graphql = makeModule('graphql', async ({ app: { onBooted }, container: { build, register, cradle }, config }) => { | ||
const { getSchema, addToSchema } = makeSchema(); | ||
|
||
onBooted(async () => { | ||
const { queries } = getSchema(); | ||
|
||
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: { | ||
registry: cradle, | ||
}, | ||
}) | ||
); | ||
}); | ||
}, 'prepend'); | ||
|
||
register({ | ||
addToSchema: asValue(addToSchema), | ||
}); | ||
}); | ||
|
||
type GraphQLRegistry = { | ||
addToSchema: AddToSchema; | ||
}; | ||
|
||
export { graphql }; | ||
export type { GraphQLRegistry }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { Registry } from '@/container'; | ||
import { GraphQLFieldConfig, GraphQLFieldResolver } from 'graphql'; | ||
|
||
type GraphQLContext = { | ||
registry: 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 }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
type Schema<Q = any, M = any> = { | ||
queries?: Q; | ||
mutations?: M; | ||
}; | ||
|
||
type AddToSchema = (schemaData: Schema) => void | ||
type GetSchema = () => Schema; | ||
|
||
type MakeSchemaStorage = { | ||
getSchema: GetSchema; | ||
addToSchema: AddToSchema; | ||
} | ||
|
||
const makeSchema = (): MakeSchemaStorage => { | ||
let queries = {}; | ||
let mutations = {}; | ||
|
||
return { | ||
getSchema: () => ({ queries, mutations }), | ||
addToSchema: (schemaData: Schema): void => { | ||
queries = { ...queries, ...schemaData.queries }; | ||
mutations = { ...mutations, ...schemaData.mutations }; | ||
}, | ||
}; | ||
}; | ||
|
||
type Dependencies = { | ||
addToSchema: AddToSchema; | ||
}; | ||
|
||
const withRegisterSchema = | ||
(schemaData: Schema) => | ||
({ addToSchema }: Dependencies): void => | ||
addToSchema(schemaData); | ||
|
||
export { makeSchema, withRegisterSchema }; | ||
export type { AddToSchema } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
|
||
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,71 @@ | ||
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(); | ||
}); | ||
|
||
describe('QUERY Articles', () => { | ||
it('gets the first page of articles', async () => { | ||
const { request, registry } = controls; | ||
const { createArticle, publishArticle } = registry; | ||
|
||
const title = randomBytes(20).toString('hex'); | ||
const content = 'New Article content'; | ||
|
||
const articleId = await createArticle({ title, content }) | ||
await publishArticle(articleId); | ||
|
||
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, | ||
}, | ||
}; | ||
|
||
return request() | ||
.post('/graphql') | ||
.send({ | ||
operationName: 'Articles', | ||
query: getArticlesQuery, | ||
variables, | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it could be interesting. Do you have something in mind? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
}); | ||
}); | ||
}); |
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.registry; | ||
const { filter, pagination, sort } = args; | ||
|
||
const { data: articles } = await findArticles({ filter, pagination, sort }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; |
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 }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.