GraphQL - defining types - graphql

I'm trying to follow Ben Awad's lireddit tutorial.
At the time he made the tutorial, there may have been different inferences about Field types.
I'm trying to add a Field to my relationship attribute (adding creator Field to a Post), so that I can then access creator attributes on that post record.
Ben does this as follows:
#Field()
#ManyToOne(() => User, (user) => user.posts)
creator: User;
That worked for him. When I try this, I get an error that says:
throw new errors_1.NoExplicitTypeError(prototype.constructor.name, propertyKey,
parameterIndex, argName);
NoExplicitTypeError: Unable to infer GraphQL type from TypeScript
reflection system. You need to provide explicit type for 'creator' of
'Post' class.
When I look at the GraphQL docs for how to provide an explicit type for creator, I can't find a similar example (simple enough for me to decipher a principle that I can apply).
I'm confused by the docs, because the have the following example:
Can anyone see what I need to do to ask for the field to be recognised as an object that I can read from?
#ObjectType()
class Rate {
#Field(type => Int)
value: number;
#Field()
date: Date;
user: User;
}
I think they use user: User the same way I use creator: User. Is there a reason that Field() can't have the same thing as ObjectType()?
I tried:
#Field(() => [User])
#ManyToOne(() => User, (user) => user.posts)
creator: User;
This doesn't give any errors (neither does the code the way Ben has it), until I get to the playground, in which case, I can't return the user data - so clearly it's wrong. It also isn't clear whether the array means the array of attributes on the user object, or an array of users (which would also be wrong). I can see from the GraphQL docs that it should be possible to define a field attribute as an object type, but I can't find an example showing how to do that.
I have seen this post, which looks like a similar problem, but I can't see from the suggested answers, how to apply those ideas to this problem.
I have seen this post, which has a similar problem, and is answered with a reference to an example that shows how to write resolvers that find relations, but my resolver already worked to find the creatorId, so I think maybe I'm not looking in the right place for an answer.
In my post resolver, I have:
import {
Resolver,
Query,
Arg,
Mutation,
InputType,
Field,
Ctx,
UseMiddleware,
Int,
FieldResolver,
Root,
ObjectType,
} from "type-graphql";
import { Post } from "../entities/Post";
import { MyContext } from "../types";
import { isAuth } from "../middleware/isAuth";
import { getConnection } from "typeorm";
#InputType()
class PostInput {
#Field()
title: string;
#Field()
text: string;
}
#ObjectType()
class PaginatedPosts {
#Field(() => [Post])
posts: Post[];
#Field()
hasMore: boolean;
}
#Resolver(Post)
export class PostResolver {
#FieldResolver(() => String)
textSnippet(#Root() post: Post) {
return post.text.slice(0, 50);
}
#Query(() => PaginatedPosts)
async posts(
#Arg("limit", () => Int) limit: number,
#Arg("cursor", () => String, { nullable: true }) cursor: string | null
): Promise<PaginatedPosts> {
// 20 -> 21
const realLimit = Math.min(50, limit);
const reaLimitPlusOne = realLimit + 1;
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.orderBy('"createdAt"', "DESC")
.take(reaLimitPlusOne);
if (cursor) {
qb.where('"createdAt" < :cursor', {
cursor: new Date(parseInt(cursor)),
});
}
const posts = await qb.getMany();
return {
posts: posts.slice(0, realLimit),
hasMore: posts.length === reaLimitPlusOne,
};
}
#Query(() => Post, { nullable: true })
post(#Arg("id") id: number): Promise<Post | undefined> {
return Post.findOne(id);
}
#Mutation(() => Post)
#UseMiddleware(isAuth)
async createPost(
#Arg("input") input: PostInput,
#Ctx() { req }: MyContext
): Promise<Post> {
return Post.create({
...input,
creatorId: req.session.userId,
}).save();
}
#Mutation(() => Post, { nullable: true })
async updatePost(
#Arg("id") id: number,
#Arg("title", () => String, { nullable: true }) title: string
): Promise<Post | null> {
const post = await Post.findOne(id);
if (!post) {
return null;
}
if (typeof title !== "undefined") {
await Post.update({ id }, { title });
}
return post;
}
#Mutation(() => Boolean)
async deletePost(#Arg("id") id: number): Promise<boolean> {
await Post.delete(id);
return true;
}
}

First of all, creator should be of a User type, and not a list of users, i.e.
#Field(() => User)
#ManyToOne(() => User, (user) => user.posts)
creator: User;
When you are retrieving a post, you should include a relation in your query, so the User entity is also loaded:
#Query(() => Post, { nullable: true })
post(#Arg("id") id: number): Promise<Post | undefined> {
return Post.findOne(id, { relations: "creator" });
}
Also, when you're using query builder to fetch posts, you should add User entities:
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.leftJoinAndSelect("p.creator", "p_creator")
.orderBy('"createdAt"', "DESC")
.take(reaLimitPlusOne);
Bonus note:
There's a common problem of over-fetching the data in GraphQL, so queries can become slow with time.
In that manner, you could also consider moving the creator field to FieldResolver, so it's retrieved from the database only if it's requested. In case you do that, one other good practice with ManyToOne relations is to use a dataloader, so if you, for example, load 10 posts from the same creator, you'll end up with only one fetching operation of that creator instead of 10 requests to the database. There's a great tutorial and explanation provided by Ben Awad too: https://www.youtube.com/watch?v=uCbFMZYQbxE.
This isn't necessary for this tutorial in particular, but it's a must-know if you're building some serious app.

Related

How to organize GraphQL resolver for additional fields

Let's say I have a simple GraphQL type for a user:
type User {
id: ID!
name: String!
}
Query {
user(id:ID!)
}
and a resolver
user = (_, {id}, {api})=> api.getUser(id)
Now I have add a new field to the User called friends and added a new resolver for the User.friends field.
friends = ({id}, _, {api})=> api.getFriends(id)
So now I wonder when we made a query like this, how can I prevent the call to api.getUser but only call api.getFriends.
query {
user(id){
friends {
name
}
}
}
My understanding is that having a resolver defined for the user field in the Query type, it will always call this resolver first and after that all resolvers for fields in the User type.
This is a common problem and there is for example this solution out there: https://github.com/gajus/graphql-lazyloader
Check out the README of the project for a structured description of your problem.
Alternatively, you can implement your own class that contains a cached value making use of how GraphQL.js implements default resolvers:
class User {
constructor(id) {
this.id = id;
}
getInstance({ api }) {
if (!this.instance) {
this.instance = api.getUser(this.id);
}
return this.instance;
}
// notice how id is already a property of this class
name(args, ctx) {
return this.getInstance(ctx).then(instance => instance.name);
}
// do the same for other fields, user will only be fetched once.
friends(args, { api }) {
return api.getFriends(this.id);
}
}
const resolvers = {
Query: {
user: (args) => new User(args.id),
}
}
If you use dataloader you can even do this with even less code thanks to caching in dataloader:
// You probably have this function already somewhere in your apollo server creation
function createContext({ api }) {
return {
api,
loaders: {
user: new Dataloader((ids) => ids.map(id => api.getUser(id))),
},
}
}
const resolvers = {
Query: {
user: (parent, args) => ({ id: args.id }),
},
User: {
name: ({ id }, args, { loaders }) =>
loaders.user.load(id).then(user => user.name),
otherProp: ({ id }, args, { loaders }) =>
loaders.user.load(id).then(user => user.otherProp),
friends: ({ id }, args, { api })=> api.getFriends(id),
}
}
Dataloader will, even when called twice, only reach to the API once. An added benefit is, that it will cache the value. Ideally, you even provide a batch load function in the API to make the loader even more efficient.
Be aware, that user.fields.name now makes calls for every friend to the API. To avoid that, you could check if the property exists:
name: (parent, args, { loaders }) =>
parent.name ?? loaders.user.load(parent.id).then(user => user.name),

Prisma and ApolloClient: Prevent overwriting the include conditions at the backend by the frontend for relations

I have a problem, thx for any help.
With prisma we can use include with where conditions for models with a relation. If I make include conditions I get the right result. If I return it to the frontend it gets overwritten. I want to return exact my result from the backend.
I have at the frontend a query (ApolloClient, gql) like. It will return an array of comments for each post, I just want to have the first Comment for each post.
const POSTS = gql`
query posts {
posts(postId: $postId) {
id
comments{ // at the backend I have conditions for the comments
id
}
}
}
`;
Backend: Primsa and graphql nexus
Prisma Schema
model Post {
id String #id #default(cuid())
comments Comment[]
}
model Comment {
id String #id #default(cuid())
post Post #relation(fields: [postId], references: [id])
postId String
}
Nexus Model
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.comments()
})
const Comment = objectType({
name: 'Comment',
definition(t) {
t.model.id()
t.model.post()
t.model.postId()
})
Resolver
export const posts = queryField('posts', {
type: 'Post',
list: true,
args: {
...
},
resolve: async (_parent, args: any, { prisma, request }, info) => {
const posts = await prisma.post.findMany({
include: {
comments: {
take: 1
}
}
})
console.log(posts)
//Perfect result I want to return the include condition. But at the frontend I have all
//comments
return posts
},
})
The console.log(posts) is exact what I want to return!. Every post has an Array of ONE Comment.
I return the posts and at the frontend every post has an Array of ALL Comments, what I don't want. How can I prevent that the frontend query overwrite the backend return? The fields are the same.
I can't add a comment, so I am adding this to another answer.
Like I said with my PrismaSelect plugin, you can't use nexus-plugin-prisma t.model, t.crud. You will need to use Pal.Js CLI to autoGenerate all CRUD and ObjectTypes for all models.
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.comments() // this field will overwritten by next one so this not needed
t.list.field('comments', {
type: 'Comment',
list: true,
resolve: (parent, args, { prisma }) => {
// here parent type include all other fields but not this field
return prisma.comment.findMany({ // this query is very wrong will case N+1 issue
where: {
postId: parent.id,
},
take: 1,
})
},
})
})
Example
model User {
id Int #default(autoincrement()) #id
createdAt DateTime #default(now())
email String #unique
name String?
password String
posts Post[]
comments Comment[]
}
model Post {
id Int #default(autoincrement()) #id
published Boolean #default(false)
title String
author User? #relation(fields: [authorId], references: [id])
authorId Int?
comments Comment[]
}
model Comment {
id Int #default(autoincrement()) #id
contain String
post Post #relation(fields: [postId], references: [id])
postId Int
author User? #relation(fields: [authorId], references: [id])
authorId Int?
}
Here is my Pal.Js CLI generated type for Post model
import { objectType } from '#nexus/schema'
export const Post = objectType({
name: 'Post',
definition(t) {
t.int('id', { nullable: false })
t.boolean('published', { nullable: false })
t.string('title', { nullable: false })
t.field('author', {
nullable: true,
type: 'User',
resolve(parent: any) {
return parent['author']
},
})
t.int('authorId', { nullable: true })
t.field('comments', {
nullable: false,
list: [true],
type: 'Comment',
args: {
where: 'CommentWhereInput',
orderBy: 'CommentOrderByInput',
cursor: 'CommentWhereUniqueInput',
take: 'Int',
skip: 'Int',
distinct: 'CommentDistinctFieldEnum',
},
resolve(parent: any) {
return parent['comments']
},
})
},
})
when you use my Pal.js CLI, your frontend request will be like this
query {
findOnePost(where: {id: 1}) {
comments(where: {}, take: 1){
id
}
}
}
``
The best way to handle this issue and just query what your frontend request to use my PrismaSelect plugin.
Prisma Select takes the info: GraphQLResolveInfo object in general graphql arguments (parent, args, context, info) to select object accepted by prisma client. The approach allows a better performance since you will only be using one resolver to retrieve all your request. By doing so, it also eliminates the N + 1 issue.
Also, you can use my CLI to autogenerate all CRUD from your schema.prisma file https://paljs.com/generator/nexus
I mean I can add to my Post-ObjectType a field condition like:
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.comments()
t.list.field('comments', {
type: 'Comment',
list: true,
resolve: (parent, args, { prisma }) => {
return prisma.comment.findMany({
where: {
postId: parent.id,
},
take: 1,
})
},
})
})
This is working. But if I understood it correct I have for every post one extra request. But I have already at the mutation resolver the right result. And I don't have the comments field at the parent (t.list.field- resolver)

setup multiple return types for the NestJs GraphQL Query decorator

I want to create a GraphQL API using NestJs. As far as I understood I won't be throwing HTTP exceptions for invalid requests anymore. Therefore I think I have to create my own "error codes" I can send back to the client. So given this basic example
#ObjectType()
export class ErrorResponse {
#Field()
message: string;
}
I have a service function to return a user by its ID and I extended the return type to return an error object if the request was invalid.
public async getUserById(id: number): Promise<ErrorResponse | User> {
const user: User = await this.usersRepository.findOne(id);
if (!user) {
const errorResponse: ErrorResponse = new ErrorResponse();
errorResponse.message = `User with ID ${id} does not exist`;
return errorResponse;
}
return user;
}
The resolver originally was something like
#Query(() => User)
public async user(#Args('id') id: number): Promise<ErrorResponse | User> {
return this.usersService.getUserById(id);
}
but as mentioned above it's also possible to return a ErrorResponse if the id does not exist. How can I design the Query decorator to provide multiple return types?
#Query(() => ErrorResponse | User)
won't do the trick and shows up with this error
The left-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint' or an enum type.ts(2362)
This is the solution that i came up for a similar situation.
GraphQL expects single return ObjectType.
First i created a common Object
#ObjectType()
export class MutationResult {
#Field({ nullable: true })
success?: boolean;
#Field({ nullable: true })
error?: boolean;
}
Then in the user module i created 2 objects types - User and UserResponse. On UserResponse i extened the common MutationResult Object
#ObjectType()
export class User {
#Field(type => ID)
id: string;
#Field()
name: string;
}
#ObjectType()
export class UserResponse extends MutationResult {
#Field()
result: User;
}
Now in query you can do this
mutation {
addUser(name: "Test") {
success,
error,
result {
name
}
}
}
If both ErrorResponse and User are an #ObjectType, you just need to "merge" them together using createUnionType.
https://docs.nestjs.com/graphql/unions-and-enums
Answer by Michal seems to be working, but the link is redirecting to some spam post. Below link is official documentation for nestjs:
https://docs.nestjs.com/graphql/unions-and-enums

GraphQL resolver for certain field not being invoked

I wrote simple GraphQL schemas and resolvers in studying purpose, and I could not get reason why my codes work as expected. I used Prisma for ORM, and express-graphql and graphql-import to compose the API.
type User {
id: ID!
name: String!
}
type Link {
id: ID!
url: String!
user: User!
}
type Query {
links(id: ID!): [Link!]!
}
// resolvers
const links = async (root, args, ctx) => {
const links = await ctx.prisma.links({
where: {
user {
id: args.id
}
}
}
}
// resolver for `Link` type
const user = async (root, args, ctx) => {
const user = await ctx.prisma.link({ id: parent.id }).user()
return user
}
// omitted field resolver for `url` and `id`
module.exports = {
user
}
With these codes, I expected to get id, url, user fields when I query links, but when I send the query, it returns null with user field. Which means, if I check in the server-side, the resolver for user field does not invoked at all. What is wrong with this?

Is it possible to add another field in the final response of GraphQL query?

I've been trying to research on how to add another root property of a GraphQL response but found nothing after 1 hour.
Normally, a GraphQL query looks like this:
{
myQuery() {
name
}
}
It responds with:
{
"data": {
"myQuery": []
}
}
I'm curious if I can add another root property in this response say "meta"
{
"data": {
"myQuery": []
},
"meta": {
"page": 1,
"count": 10,
"totalItems": 90
}
}
Is this possible, if not what's the best approach in tackling this with respect to GraphQL?
Thanks!
The apollo-server middleware can be configured with a number of configuration options, including a formatResponse function that allows you to modify the outgoing GraphQL response
const formatResponse = (response) => {
return {
meta
...response
}
}
app.use('/graphql', bodyParser.json(), graphqlExpress({
schema,
formatResponse,
}));
You could pass the req object down to your context, mutate it within your resolver(s) and then use the result inside formatResponse. Something like...
app.use('/graphql', bodyParser.json(), (req, res, next) => graphqlExpress({
schema,
formatResponse: (gqlResponse) => ({
...gqlResponse
meta: req.metadata
}),
})(req, res, next));
Typically, though, you would want to include the metadata as part of your actual schema and have it included with the data. That will also allow you to potentially request multiple queries and get the metadata for all of them.
There's any number of ways to do that, depending on how your data is structured, but here's an example:
type Query {
getFoos: QueryResponse
getBars: QueryResponse
}
type QueryResponse {
results: [Result]
meta: MetaData
}
union Result = Bar | Foo
You can add anything in the response as well... Please follow below code.
app.use('/graphql', bodyParser.json(), graphqlExpress(req => {
return {
schema: tpSchemaNew,
context: {
dbModel
},
formatError: err => {
if (err.originalError && err.originalError.error_message) {
err.message = err.originalError.error_message;
}
return err;
},
formatResponse : res => {
res['meta'] = 'Hey';
return res;
}
}
}))
Apollo Server-specific:
Just adding to the previous answers that formatResponse() has another useful argument, requestContext.
If you are interested in extracting values from that (for example, the context passed to the resolver), you can do the following. BEWARE HOWEVER, the context will likely contain sensitive data that is supposed to be private. You may be leaking authentication data and secrets if not careful.
const server = new ApolloServer({
schema,
formatResponse: (response, requestContext) => {
//return response
const userId = requestContext.context.user.id
response = Object.assign(response, {
extensions: {
meta: {
userId: userId
}
}
}
return response
},
})
The above will return something like this in the gql query response (note the extensions object):
{
data: {
user: {
firstName: 'Hello',
lastName: 'World'
}
},
extensions: { // <= in Typescript, there is no `meta` in GraphQLResponse, but you can use extensions
meta: {
userId: 1234 //<= data from the context
}
}
}
The full list of properties available in requestContext:
at node_modules/apollo-server-types/src/index.ts>GraphQLRequestContext
export interface GraphQLRequestContext<TContext = Record<string, any>> {
readonly request: GraphQLRequest;
readonly response?: GraphQLResponse;
readonly context: TContext;
readonly cache: KeyValueCache;
// This will be replaced with the `operationID`.
readonly queryHash?: string;
readonly document?: DocumentNode;
readonly source?: string;
// `operationName` is set based on the operation AST, so it is defined even if
// no `request.operationName` was passed in. It will be set to `null` for an
// anonymous operation, or if `requestName.operationName` was passed in but
// doesn't resolve to an operation in the document.
readonly operationName?: string | null;
readonly operation?: OperationDefinitionNode;
/**
* Unformatted errors which have occurred during the request. Note that these
* are present earlier in the request pipeline and differ from **formatted**
* errors which are the result of running the user-configurable `formatError`
* transformation function over specific errors.
*/
readonly errors?: ReadonlyArray<GraphQLError>;
readonly metrics?: GraphQLRequestMetrics;
debug?: boolean;
}

Resources