TypeGraphQL - Not able to match all the interfaces of a union - graphql

Summary
The goal is to declare the return type of a mutation using a union in order to express multiple states: Success and user errors
Being able to select concrete types according to the use cases:
mutation($data: CreateUserInput!) {
createUser(data: $data){
... on CreateUserSuccess {
user {
id
}
}
... on EmailTakenError {
emailWasTaken
}
... on UserError {
code
message
}
}
}
Implementation using TypeGraphQ:
#ObjectType()
class CreateUserSuccess {
#Field(() => User)
user: User
}
#ObjectType()
class EmailTakenError {
#Field()
emailWasTaken: boolean
}
const mapMutationValueKeyToObjectType = {
user: CreateUserSuccess,
code: UserError,
emailWasTaken: EmailTakenError
}
const CreateUserPayload = createUnionType({
name: 'CreateUserPayload',
types: () => [CreateUserSuccess, EmailTakenError, UserError] as const,
resolveType: mutationValue => {
const mapperKeys = Object.keys(mapMutationValueKeyToObjectType)
const mutationValueKey = mapperKeys.find((key) => key in mutationValue)
return mapMutationValueKeyToObjectType[mutationValueKey]
}
})
#InputType()
class CreateUserInput implements Partial<User> {
#Field()
name: string
#Field()
email: string
}
#Resolver(User)
export class UserResolver {
#Mutation(() => CreateUserPayload)
createUser (#Arg('data', {
description: 'Represents the input data needed to create a new user'
}) createUserInput: CreateUserInput) {
const { name, email } = createUserInput
return createUser({ name, email })
}
}
Data layer
export const createUser = async ({
name, email
}: { name: string; email: string; }) => {
const existingUser = await dbClient.user.findUnique({
where: {
email
}
})
if (existingUser) {
return {
code: ErrorCode.DUPLICATE_ENTRY,
message: "There's an existing user with the provided email.",
emailWasTaken: true
}
}
return dbClient.user.create({
data: {
name,
email
}
})
}
Issue
The response doesn't resolve all of the selected fields according to their unions, even by returning fields that are related to different types
if (existingUser) {
return {
code: ErrorCode.DUPLICATE_ENTRY,
message: "There's an existing user with the provided email.",
emailWasTaken: true
}
}
My doubt is this case is, why emailWasTaken is not being returned within the response if the EmailTakenError type is being selected?

This was an interpretation mistake on my part
The reasoning is that resolvers with a union type as the return definition should indeed just return one of those, in the case above, UserError and EmailTakenError wouldn't be returned on the same response
More info on this GitHub discussion

Related

Prisma / Graphql resolver

Good morning all,
I m currently back in the famous world of web development and I have in mind to develop a tool by using Nest/Prisma/Graphl.
However, I'm struggling a little bit on key element like the following one.
Basically, I can see that, by using the "include" function in Prisma (module.service.ts), I'm getting subModules list: this is the expected behavior.
However, on Graphl side, to cover field resolver (module.resolver.ts), I can see that the same request is executing again to cover SubModules field.....
What am I missing?????
See below the code:
module.module.ts
import { Field, ID, ObjectType } from '#nestjs/graphql'
import { SubModule } from './submodule.model'
#ObjectType()
export class Module {
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Field((type) => ID)
id: number
name: string
description: string
icon: string
active: boolean
position: number
subModules?: SubModule[]
}
submodule.model.ts
import { Field, ObjectType, ID } from '#nestjs/graphql';
import { Module } from './module.model';
#ObjectType()
export class SubModule {
#Field((type) => ID)
id: number;
name: string;
description: string;
icon: string;
active: boolean;
position: number;
module: Module;
}
module.resolver.ts
import {
Resolver,
Query,
ResolveField,
Parent,
Args,
InputType,
} from '#nestjs/graphql'
import { Module } from 'src/models/module.model'
import { ModuleService } from './module.service'
import { SubModuleService } from './sub-module.service'
#InputType()
class FilterModules {
name?: string
description?: string
icon?: string
active?: boolean
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Resolver((of) => Module)
export class ModuleResolver {
constructor(
private moduleService: ModuleService,
private subModuleService: SubModuleService,
) {}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Query((returns) => Module)
async module(#Args('ModuleId') id: number) {
return this.moduleService.module(id)
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Query((returns) => [Module], { nullable: true })
async modules(
#Args({ name: 'skip', defaultValue: 0, nullable: true }) skip: number,
#Args({ name: 'filterModules', defaultValue: '', nullable: true })
filterModules: FilterModules,
) {
return this.moduleService.modules({
skip,
where: {
name: {
contains: filterModules.name,
},
},
})
}
#ResolveField()
async subModules(#Parent() module: Module) {
const { id } = module
return this.subModuleService.subModules({ where: { moduleId: id } })
}
}
module.service.ts
import { Injectable } from '#nestjs/common'
import { PrismaService } from 'src/prisma.service'
import { Prisma, Module } from '#prisma/client'
#Injectable()
export class ModuleService {
constructor(private prisma: PrismaService) {
prisma.$on<any>('query', (event: Prisma.QueryEvent) => {
console.log('Query: ' + event.query)
console.log('Params' + event.params)
console.log('Duration: ' + event.duration + 'ms')
})
}
async module(id: number): Promise<Module | null> {
return this.prisma.module.findUnique({
where: {
id: id || undefined,
},
})
}
async modules(params: {
skip?: number
take?: number
cursor?: Prisma.ModuleWhereUniqueInput
where?: Prisma.ModuleWhereInput
orderBy?: Prisma.ModuleOrderByWithRelationInput
}): Promise<Module[]> {
const { skip, take, cursor, where, orderBy } = params
return this.prisma.module.findMany({
skip,
take,
cursor,
where,
orderBy,
})
}
async updateModule(params: {
where: Prisma.ModuleWhereUniqueInput
data: Prisma.ModuleUpdateInput
}): Promise<Module> {
const { where, data } = params
return this.prisma.module.update({
data,
where,
})
}
}
Thanks in advance for your help

TypeGraphql - #inputtype on typeorm

Hello I need to check if there is an email in the database already:
with this:
return User.findOne({ where: { email } }).then((user) => {
if (user) return false;
return true;
});
I have the following inputtypes:
#InputType()
export class RegisterInput {
#Field()
#IsEmail({}, { message: 'Invalid email' })
email: string;
#Field()
#Length(1, 255)
name: string;
#Field()
password: string;
}
I would like to know if there is any way for me to validate the email in the inputtype? or just in my resolve:
#Mutation(() => User)
async register(
#Arg('data')
{ email, name, password }: RegisterInput,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 12);
const user = await User.create({
email,
name,
password: hashedPassword,
}).save();
return user;
}
Actually you can register your own decorator for class-validator
For example it can look something like this:
isEmailAlreadyExists.ts
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { UserRepo } from '../../repositories/UserRepo';
import { InjectRepository } from 'typeorm-typedi-extensions';
#ValidatorConstraint({ async: true })
export class isEmailAlreadyExist
implements ValidatorConstraintInterface {
#InjectRepository()
private readonly userRepo: UserRepo;
async validate(email: string) {
const user = await this.userRepo.findOne({ where: { email } });
if (user) return false;
return true;
}
}
export function IsEmailAlreadyExist(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: isEmailAlreadyExist,
});
};
}
If you're injecting dependencies than you should in inject it in class-validator too. Simply add to your main file this:
import { Container } from 'typedi';
import * as classValidator from 'class-validator';
classValidator.useContainer(Container);
...
const schema = await buildSchema({
resolvers: [...],
container: Container,
});
Then you can use decorator in your InputType
import { InputType, Field } from 'type-graphql';
import { IsEmailAlreadyExist } from '../../../utils/validators/isEmailAlreadyExist';
#InputType()
export class YourInput {
#Field()
#IsEmailAlreadyExist()
email: string;
}
I actually just figured this out myself for my own project.
You can simply add a validation on the email from RegisterInput argument and throw an error if the email already exists.
import { Repository } from 'typeorm'
import { InjectRepository } from 'typeorm-typedi-extensions'
...
// Use dependency injection in the resolver's constructor
constructor(
#InjectRepository(User) private readonly userRepository: Repository<User>
) {}
...
// Your mutation
#Mutation(() => User)
async register(
#Arg('data')
{ email, name, password }: RegisterInput,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 12);
const userWithEmail = this.userRepository.find({ email: email })
// If a user with the email was found
if (userWithEmail) {
throw new Error('A user with that email already exists!')
}
const user = await User.create({
email,
name,
password: hashedPassword,
}).save();
return user;
}
To use the InjectRepository make sure you add a "container" to your buildSchema function:
import { Container } from 'typedi'
...
const schema = await buildSchema({
resolvers: [...],
container: Container
})
Let me know if this works out for you? Thanks!

throw a descriptive error with graphql and apollo

Consider the following class:
// entity/Account.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import { Field, Int, ObjectType } from 'type-graphql'
#ObjectType()
#Entity()
export class Account extends BaseEntity {
#Field(() => Int)
#PrimaryGeneratedColumn()
id: number
#Field()
#Column({ length: 50, unique: true })
#Index({ unique: true })
accountIdentifier: string
#Field({ nullable: true })
#Column({ length: 100 })
name?: string
}
With it's corresponding resolver:
// AccountResolver.ts
#Resolver()
export class AccountResolver {
#Mutation(() => Account)
async addAccount(#Arg('options', () => AccountInput) options: AccountInput) {
try {
// if (!options.accountIdentifier) {
// throw new Error(`Failed adding account: the accountIdentifier is missing`)
// }
return await Account.create(options).save()
} catch (error) {
if (error.message.includes('Cannot insert duplicate key')) {
throw new Error(
`Failed adding account: the account already exists. ${error}`
)
} else {
throw new Error(`Failed adding account: ${error}`)
}
}
}
}
Jest test file
// AccountResolver.test.ts
describe('the addAccount Mutation', () => {
it('should throw an error when the accountIdentifier is missing', async () => {
await expect(
client.mutate({
mutation: gql`
mutation {
addAccount(
options: {
name: "James Bond"
userName: "James.Bond#contoso.com"
}
) {
accountIdentifier
}
}
`,
})
).rejects.toThrowError('the accountIdentifier is missing')
})
The field accountIdentifier is mandatory and should throw a descriptive error message when it's missing in the request. However, the error thrown is:
"Network error: Response not successful: Received status code 400"
What is the correct way to modify the error message? I looked at type-graphql with the class-validators and made sure that validate: true is set but it doesn't give a descriptive error.
EDIT
After checking the graphql playground, it does show the correct error message by default. The only question remaining is how write the jest test so it can read this message:
{
"error": {
"errors": [
{
"message": "Field AccountInput.accountIdentifier of required type String! was not provided.",
Thank you for any help you could give me.
The ApolloError returned by your client wraps both the errors returned in the response and any network errors encountered while executing the request. The former is accessible under the graphQLErrors property, the latter under the networkError property. Instea dof using toThrowError, you should use toMatchObject instead:
const expectedError = {
graphQLErrors: [{ message: 'the accountIdentifier is missing' }]
}
await expect(client.mutate(...)).rejects.toMatchObject(expectedError)
However, I would suggest avoiding using Apollo Client for testing. Instead, you can execute operations directly against your schema.
import { buildSchema } from 'type-graphql'
import { graphql } from 'graphql'
const schema = await buildSchema({
resolvers: [...],
})
const query = '{ someField }'
const context = {}
const variables = {}
const { data, errors } = await graphql(schema, query, {}, context, variables)

Access return data from resolver in graphql

I want to access the country field from my resolver. The country is being returned by query but since Product is a list I can only access the object inside items return by query. Is there any way I can have access to whole returned data from query or any way to pass it further down as an argument to my resolver function
//schema
type ProductCollectionPage {
items: [Product!]!
}
//resolver
const resolvers = {
Product: {
variants: async (obj: any, args: any, { dataSources }: any): Promise<IProductVariantPage> => {
const { id } = obj;
// want to access country here
return (dataSources.xyz as XyzRepository).retriveProducts(country, id);
}
},
Query: {
products: async (
obj: any,
{ id }: { id: string },
{ dataSources }: any
): Promise<
any
> => {
const locationDetails = await (dataSources.abc as InventoryLocationsRepository).retrieveInventoryLocation(id);
const country = locationDetails.country;
const response = await (dataSources.abc as XyzRepository).retriveProductIds(country);
// response.list === [{id: 1}, {id:2}]
return {
country,
items: response.list
}
}
}
};
As arrays are objects in javascript then you can just assign additional property to response.list:
response.list.country = country;

GraphQL Subscriptions return an empty (null) response [duplicate]

I have the following GRAPHQL subscription:
Schema.graphql
type Subscription {
booking: SubscriptionData
}
type SubscriptionData {
booking: Booking!
action: String
}
And this is the resolver subsrciption file
Resolver/Subscription.js
const Subscription = {
booking: {
subscribe(parent, args, { pubsub }, info) {
return pubsub.asyncIterator("booking");
}
}
};
export default Subscription;
Then I have the following code on the Mutation in question
pubsub.publish("booking", { booking: { booking }, action: "test" });
I have the follow subscription file in front end (React)
const getAllBookings = gql`
query {
bookings {
time
durationMin
payed
selected
activity {
name
}
}
}
`;
const getAllBookingsInitial = {
query: gql`
query {
bookings {
time
durationMin
payed
selected
activity {
name
}
}
}
`
};
class AllBookings extends Component {
state = { allBookings: [] }
componentWillMount() {
console.log('componentWillMount inside AllBookings.js')
client.query(getAllBookingsInitial).then(res => this.setState({ allBookings: res.data.bookings })).catch(err => console.log("an error occurred: ", err));
}
componentDidMount() {
console.log(this.props.getAllBookingsQuery)
this.createBookingsSubscription = this.props.getAllBookingsQuery.subscribeToMore(
{
document: gql`
subscription {
booking {
booking {
time
durationMin
payed
selected
activity {
name
}
}
action
}
}
`,
updateQuery: async (prevState, { subscriptionData }) => {
console.log('subscriptionData', subscriptionData)
const newBooking = subscriptionData.data.booking.booking;
const newState = [...this.state.allBookings, newBooking]
this.setState((prevState) => ({ allBookings: [...prevState.allBookings, newBooking] }))
this.props.setAllBookings(newState);
}
},
err => console.error(err)
);
}
render() {
return null;
}
}
export default graphql(getAllBookings, { name: "getAllBookingsQuery" })(
AllBookings
);
And I get the following response:
data: {
booking: {booking: {...} action: null}}
I get that I am probably setting up the subscription wrong somehow but I don't see the issue.
Based on your schema, the desired data returned should look like this:
{
"booking": {
"booking": {
...
},
"action": "test"
}
}
The first booking is the field on Subscription, while the second booking is the field on SubscriptionData. The object you pass to publish should have this same shape (i.e. it should always include the root-level subscription field).
pubsub.publish('booking', {
booking: {
booking,
action: 'test',
},
})

Resources