Nest Error: Cannot determine GraphQL output type for getProducts - graphql

I get this error when i try to return a custom dto from a resolver. (node:28156) UnhandledPromiseRejectionWarning: Error: Cannot determine GraphQL output type for getProducts
Here is what my code looks like.
product.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
BaseEntity,
BeforeInsert,
} from 'typeorm';
import { ObjectType, Field, ID, Float, Int } from '#nestjs/graphql';
#ObjectType()
#Entity()
export class Product extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn('uuid')
id: string;
#Column({ default: '', select: false })
ratingsString: string;
#Field(() => [Int])
ratings;
#BeforeInsert()
setRatingsString() {
this.ratingsString = this.ratings.join(',');
}
}
product.resolver.ts
import { Resolver, Query, Args, ResolveField, Int, Parent } from '#nestjs/graphql';
import { Product } from './product.entity';
import { ProductService } from './product.service';
#Resolver(() => Product)
export class ProductResolver {
constructor(private productService: ProductService) {}
#Query(() => GetProductDto, { name: 'products' })
async getProducts(#Args() page: number) {
return this.productService.getProducts(page);
}
}
product.service.ts
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { GetProductDto } from './product.dto';
#Injectable()
export class ProductService {
constructor(
#InjectRepository(Product) private productRepo: Repository<Product>,
) {}
async getProducts(page: number): Promise<GetProductDto> {
const MAX_PRODUCT_PER_PAGE = 20;
const start = page < 1 ? 1 : page;
const [products, productsCount] = await this.productRepo.findAndCount({
skip: (start - 1) * MAX_PRODUCT_PER_PAGE,
take: MAX_PRODUCT_PER_PAGE,
});
const productList = products.map(product => {
product.ratings = product.ratingsString.split(',').map(Number);
delete product.ratingsString;
return product;
});
return {
products: productList,
total: productsCount,
};
}
}
product.dto.ts
import { Field, Int, InputType } from '#nestjs/graphql';
import { Product } from './product.entity';
#InputType()
export class GetProductDto {
#Field(() => [Product])
products: Product[];
#Field(() => Int)
total: number;
}
The expectation here is that when i call the products query i will recieve an object with products and total. Whee products will be my array of products. What am I doing wrong?

Turns out all I needed was to provide a argument to Args.
#Query(() => GetProductDto, { name: 'products' })
async getProducts(#Args('arg_the_client_will_it_with') page: number) {
return this.productService.getProducts(page);
}

Related

GraphQL - How can I tell if my delete mutation has been created?

My react, apollo, prisma, nextjs typescript app seems to think I have not made a deleteIssueGroup mutation. I think I have.
I am using:
"devDependencies": {
"#graphql-codegen/add": "3.2.1",
"#graphql-codegen/cli": "2.13.7",
"#graphql-codegen/typescript": "2.8.0",
"#graphql-codegen/typescript-operations": "2.5.5",
"#graphql-codegen/typescript-react-apollo": "3.3.5",
"#types/cookie": "0.5.1",
"#types/react": "17.0.51",
"#types/react-dom": "17.0.17",
"eslint-config-next": "12.3.1"
It's strange because I defined a deleteGroup mutation at the same time as I made the createGroup mutation and the AllGroups query, but my app thinks I don't have a deleteGroup mutation (it's right, the #generated file doesn't have it in there - except for it being listed in the list of mutations - there are no individual line items in graphql.tsx that define useDeleteIssueMutation in the same way that there are such lines for the create type). I don't know why. Is there a way to force the creation, or the recognition that there is only one of everything?
I can see in my graphql.tsx that I have:
export type MutationDeleteGroupArgs = {
id: Scalars['String'];
};
which I think means I should have the delete mutation, but in the form, when I'm trying to use it, I get an error in the terminal that says it doesn't exist.
When I run my yarn db:migrate script, it goes through the motions of running the prisma migrations (they are all up to date, but it then runs the code gen and concludes successfully).
My code is (I made a new one called IssueGroup to try and see if it was just a random unlucky twist that is causing this mess):
Prisma model
model IssueGroup {
id String #id #default(dbgenerated("gen_random_uuid()")) #db.Uuid
title String
description String
issues Issue[]
createdAt DateTime #default(now()) #db.Timestamptz(6)
updatedAt DateTime #default(now()) #updatedAt #db.Timestamptz(6)
}
model
import * as Prisma from "#prisma/client"
import { Field, ObjectType } from "type-graphql"
import { BaseModel } from "../shared/base.model"
#ObjectType()
export class IssueGroup extends BaseModel implements Prisma.IssueGroup {
#Field()
title: string
#Field()
description: string
#Field(type => String)
issues: Prisma.Issue[];
}
group service:
import { prisma } from "../../lib/prisma"
import { Service } from "typedi"
import { IssueGroupInput } from "./inputs/create.input"
import { Resolver } from "type-graphql"
import { IssueGroup } from "./issueGroup.model"
#Service()
#Resolver(() => IssueGroup)
export class IssueGroupService {
async createIssueGroup(data: IssueGroupInput) {
return await prisma.issueGroup.create({
data,
})
}
async deleteIssueGroup(id: string) {
return await prisma.issueGroup.delete({ where: { id } })
}
async updateIssueGroup(id: string, data: IssueGroupInput) {
const issueGroup = await prisma.issueGroup.findUnique({ where: { id } })
if (!issueGroup) {
throw new Error("Issue not found")
}
return await prisma.issueGroup.update({ where: { id }, data })
}
async getAllIssueGroups() {
return await (await prisma.issueGroup.findMany({orderBy: {title: 'asc'}}))
}
async getIssueGroup(id: string) {
return await prisma.issueGroup.findUnique({
where: {
id,
},
})
}
}
resolver
import { Arg, Mutation, Query, Resolver } from "type-graphql"
import { IssueGroup } from "./issueGroup.model"
import { IssueGroupService } from "./issueGroup.service"
import { IssueGroupInput } from "./inputs/create.input"
import { Inject, Service } from "typedi"
#Service()
#Resolver(() => IssueGroup)
export default class IssueGroupResolver {
#Inject(() => IssueGroupService)
issueGroupService: IssueGroupService
#Query(() => [IssueGroup])
async issueGroups() {
return await this.issueGroupService.getAllIssueGroups()
}
#Query(() => IssueGroup)
async issueGroup(#Arg("id") id: string) {
return await this.issueGroupService.getIssueGroup(id)
}
#Mutation(() => IssueGroup)
async createIssueGroup(#Arg("data") data: IssueGroupInput) {
return await this.issueGroupService.createIssueGroup(data)
}
// : Promise<IssueGroup[]> {
#Mutation(() => IssueGroup)
async updateIssueGroup(
#Arg("id") id: string,
#Arg("data") data: IssueGroupInput
) {
return this.issueGroupService.updateIssueGroup(id, data)
}
#Mutation(() => IssueGroup)
async deleteIssueGroup(#Arg("id") id: string) {
return this.issueGroupService.deleteIssueGroup(id)
}
}
I have seen this post and can see the warning about naming queries uniquely - but I cannot see how I have offended the principle.
I have seen this post and tried following the suggestion to change my codegen.yml from:
documents:
- "src/components/**/*.{ts,tsx}"
- "src/lib/**/*.{ts,tsx}"
- "src/pages/**/*.{ts,tsx}"
to:
documents:
- "src/components/**/!(*.types).{ts,tsx}"
- "src/lib/**/!(*.types).{ts,tsx}"
- "src/pages/**/!(*.types).{ts,tsx}"
but I still get the same issue - my vsCode is suggesting I might want to use create instead of delete.

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!

GraphQL endpoint return null object in Nest.js

I'm using Nest.js and Sequelize-Typescript to build a GraphQL API.
When I called delete and update mutations I got a null object, but the operation it is done. I need to put {nullable: true} because I got a error saying Cannot return null for non-nullable field . How I fix it? I need the endpoint to return the updated object to show the information on the front
error img
book.dto.ts
import { ObjectType, Field, Int, ID } from 'type-graphql';
#ObjectType()
export class BookType {
#Field(() => ID, {nullable: true})
readonly id: number;
#Field({nullable: true})
readonly title: string;
#Field({nullable: true})
readonly author: string;
}
book.resolver.ts
import {Args, Mutation, Query, Resolver} from '#nestjs/graphql';
import { Book } from './model/book.entity';
import { BookType } from './dto/book.dto';
import { CreateBookInput } from './input/createBook.input';
import { UpdateBookInput } from './input/updateBook.input';
import { BookService } from './book.service';
#Resolver('Book')
export class BookResolver {
constructor(private readonly bookService: BookService) {}
#Query(() => [BookType])
async getAll(): Promise<BookType[]> {
return await this.bookService.findAll();
}
#Query(() => BookType)
async getOne(#Args('id') id: number) {
return await this.bookService.find(id);
}
#Mutation(() => BookType)
async createItem(#Args('input') input: CreateBookInput): Promise<Book> {
const book = new Book();
book.author = input.author;
book.title = input.title;
return await this.bookService.create(book);
}
#Mutation(() => BookType)
async updateItem(
#Args('input') input: UpdateBookInput): Promise<[number, Book[]]> {
return await this.bookService.update(input);
}
#Mutation(() => BookType)
async deleteItem(#Args('id') id: number) {
return await this.bookService.delete(id);
}
#Query(() => String)
async hello() {
return 'hello';
}
}
book.service.ts
import {Inject, Injectable} from '#nestjs/common';
import {InjectRepository} from '#nestjs/typeorm';
import {Book} from './model/book.entity';
import {DeleteResult, InsertResult, Repository, UpdateResult} from 'typeorm';
#Injectable()
export class BookService {
constructor(#Inject('BOOKS_REPOSITORY') private readonly bookRepository: typeof Book) {}
findAll(): Promise<Book[]> {
return this.bookRepository.findAll<Book>();
}
find(id): Promise<Book> {
return this.bookRepository.findOne({where: {id}});
}
create(data): Promise<Book> {
return data.save();
}
update(data): Promise<[number, Book[]]> {
return this.bookRepository.update<Book>(data, { where: {id: data.id} });
}
delete(id): Promise<number> {
return this.bookRepository.destroy({where: {id}});
}
}
You can fix it setting option parameter in the resolver query
#Query(() => BookType, { nullable: true })
Why would you want to return those fields from a delete? You must already have them on your front end... you could just change the return type of that mutation to true or false based on whether it worked or not... and in the update you could do the mutation and add returning: true in your options if you are using postgres... if not then don't return the result of the update, do the update and return the result of findOne or findById whichever is applicable

Angular 2 - Test of a component

I am building a basic CRUD application in Angular2. However I am having some issues while running tests of components.
Component Code:
///<reference path="../../node_modules/angular2/typings/browser.d.ts"/>
import { Component, OnInit } from 'angular2/core';
import { RouteParams, Router, ROUTER_DIRECTIVES } from 'angular2/router';
import { EmployeeEditFormComponent } from './employee-edit-form.component';
import { EmployeeDetailServiceComponent } from '../services/employee-detail-service.component';
import { EmployeeDeleteServiceComponent } from '../services/employee-delete-service.component';
#Component({
selector: 'employee-detail',
templateUrl: 'src/pages/employee-detail.component.html',
providers: [
EmployeeDetailServiceComponent,
EmployeeDeleteServiceComponent
],
directives: [ ROUTER_DIRECTIVES, EmployeeEditFormComponent ]
})
export class EmployeeDetailComponent implements OnInit {
public currentEmployee;
public errorMessage: string;
constructor(
private _router: Router,
private _routeParams: RouteParams,
private _detailService: EmployeeDetailServiceComponent,
private _deleteService: EmployeeDeleteServiceComponent
){}
ngOnInit() {
let id = parseInt(this._routeParams.get('id'));
this._detailService.getEmployee(id).subscribe(
employee => this.currentEmployee = employee,
error => this.errorMessage = <any>error
);
}
deleteHandler(id: number) {
this._deleteService.deleteEmployee(id).subscribe(
employee => this.currentEmployee = employee,
errorMessage => this.errorMessage = errorMessage,
() => this._router.navigate(['EmployeeList'])
)
}
}
Spec Code:
/// <reference path="../../typings/main/ambient/jasmine/jasmine.d.ts" />
import {
it,
describe,
expect,
TestComponentBuilder,
injectAsync,
setBaseTestProviders,
beforeEachProviders
} from "angular2/testing";
import {
Response,
XHRBackend,
ResponseOptions,
HTTP_PROVIDERS
} from "angular2/http";
import {
MockConnection,
MockBackend
} from "angular2/src/http/backends/mock_backend";
import {
TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS
} from "angular2/platform/testing/browser";
import {
Component,
provide
} from "angular2/core";
import {
RouteParams
} from 'angular2/router';
import 'rxjs/Rx';
import { Employee } from '../models/employee';
import { EmployeeDetailComponent } from './employee-detail.component';
import { EmployeeEditFormComponent } from './employee-edit-form.component';
import { EmployeeDetailServiceComponent } from '../services/employee-detail-service.component';
import { EmployeeDeleteServiceComponent } from '../services/employee-delete-service.component';
class MockDetailService{
public getEmployee (id: number) {
return new Employee(1, "Abhinav Mishra");
}
}
class MockDeleteService{
public deleteEmployee (id: number) {
return new Employee(1, "Abhinav Mishra");
}
}
describe('Employee Detail Component Tests', () => {
setBaseTestProviders(
TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS
);
beforeEachProviders(() => {
return [
HTTP_PROVIDERS,
provide(XHRBackend, {useClass: MockBackend}),
provide(RouteParams, { useValue: new RouteParams({ id: '1' }) }),
provide(EmployeeDetailServiceComponent, {useClass: MockDetailService}),
provide(EmployeeDeleteServiceComponent, {useClass: MockDeleteService})
]
});
it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb
.overrideProviders(EmployeeDetailComponent,
[
provide(EmployeeDetailServiceComponent, {useClass: MockDetailService}),
provide(EmployeeDeleteServiceComponent, {useClass: MockDeleteService})
]
)
.createAsync(EmployeeDetailComponent).then((componentFixture) => {
componentFixture.detectChanges();
expect(true).toBe(true);
});
}));
});
I keep getting following error:
Error: XHR error (404 Not Found) loading http://localhost:9876/angular2/router
at error (/home/abhi/Desktop/angular2-testing/node_modules/systemjs/dist/system.src.js:1026:16)
at XMLHttpRequest.xhr.onreadystatechange (/home/abhi/Desktop/angular2-testing/node_modules/systemjs/dist/system.src.js:1047:13)
at XMLHttpRequest.wrapFn [as _onreadystatechange] (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:771:30)
at ZoneDelegate.invokeTask (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:365:38)
at Zone.runTask (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:263:48)
at XMLHttpRequest.ZoneTask.invoke (/home/abhi/Desktop/angular2-testing/node_modules/angular2/bundles/angular2-polyfills.js:431:34)
Would be great to have some feedbacks.

Resources