GraphQL with Prisma: how to make a delete mutation - graphql

I am trying to figure out how to make a delete mutation in my app.
I have an Issue Service, Issue Resolver, and Issue Form.
The service has:
import { prisma } from "../../lib/prisma"
import { Service } from "typedi"
import { IssueInput } from "./inputs/create.input"
import { Resolver } from "type-graphql"
import { Issue } from "./issue.model"
#Service()
#Resolver(() => Issue)
export class IssueService {
async createIssue(data: IssueInput) {
return await prisma.issue.create({ data })
}
async getAllIssues() {
return await prisma.issue.findMany()
}
async getIssue(id: string) {
return await prisma.issue.findUnique({ where: { id } })
}
async deleteIssue(id: string) {
return await prisma.issue.delete({ where: { id } })
}
}
The resolver has:
import { Arg, Mutation, Query, Resolver } from "type-graphql"
import { Issue } from "./issue.model"
import { IssueService } from "./issue.service"
import { IssueInput } from "./inputs/create.input"
import { Inject, Service } from "typedi"
#Service()
#Resolver(() => Issue)
export default class IssueResolver {
#Inject(() => IssueService)
issueService: IssueService
#Query(() => [Issue])
async allIssues() {
return await this.issueService.getAllIssues()
}
#Query(() => Issue)
async issue(#Arg("id") id: string) {
return await this.issueService.getIssue(id)
}
#Mutation(() => Issue)
async createIssue(#Arg("data") data: IssueInput) {
return await this.issueService.createIssue(data)
}
#Mutation(() => Issue)
async deleteIssue(#Arg("id") id: string) {
return await this.issueService.deleteIssue(id)
}
}
My lib/graphql sort of recognises that I tried to make the mutations. It has entries as follows:
export type Mutation = {
__typename?: 'Mutation';
createIssue: Issue;
deleteIssue: Issue;
export type MutationDeleteIssueArgs = {
id: Scalars['String'];
};
Apart from the above two references, the lib/graphql does not have something that looks like the other mutations and queries.
In the issue form, I want to try and use the delete mutation, so I tried:
import {
IssueInput,
useAllIssuesQuery,
useCreateIssueMutation,
useDeleteIssueMutation, ## this is not recognised as an exported member of lib/graphql
} from "lib/graphql"
How can I add it to the lib/graphql. Whatever the process for generating the mutation was has obviously run, but I don't understand why it didn't make something I can use in the form.
My goal is to try and figure out how to write something along the following lines to try and delete an entry.
{ onDelete: async (id) => {
await fetch("api/issue", { method: "delete" }),
toast({
title: "Issue deleted",
})
refetch()
refetchClimate()
}
}

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.

How to upload file using nestjs-graphql-fastify server and how to test such feature?

I struggle to upload .csv file to nestjs-graphql-fastify server. Tried following code:
#Mutation(() => Boolean)
async createUsers(
#Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<boolean> {
try {
// backend logic . . .
} catch {
return false;
}
return true;
}
but all I get when testing with postman is this response:
{
"statusCode": 415,
"code": "FST_ERR_CTP_INVALID_MEDIA_TYPE",
"error": "Unsupported Media Type",
"message": "Unsupported Media Type: multipart/form-data; boundary=--------------------------511769018912715357993837"
}
Developing with code-first approach.
Update: Tried to use fastify-multipart but issue still remains. What has changed is response in postman:
POST body missing, invalid Content-Type, or JSON object has no keys.
Found some answer's on Nestjs discord channel.
You had to do following changes:
main.ts
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastify = adapter.getInstance();
fastify.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastify.addHook('preValidation', async function (request: any, reply) {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
);
await app.listen(apiServerPort, apiServerHost);
}
bootstrap();
upload.scalar.ts
import { Scalar } from '#nestjs/graphql';
import { GraphQLUpload } from 'graphql-upload';
#Scalar('Upload')
export class UploadGraphQLScalar {
protected parseValue(value) {
return GraphQLUpload.parseValue(value);
}
protected serialize(value) {
return GraphQLUpload.serialize(value);
}
protected parseLiteral(ast) {
return GraphQLUpload.parseLiteral(ast, ast.value);
}
}
users.resolver.ts
#Mutation(() => CreateUsersOutput, {name: 'createUsers'})
async createUsers(
#Args('input', new ValidationPipe()) input: CreateUsersInput,
#ReqUser() reqUser: RequestUser,
): Promise<CreateUsersOutput> {
return this.usersService.createUsers(input, reqUser);
}
create-shared.input.ts
#InputType()
export class DataObject {
#Field(() => UploadGraphQLScalar)
#Exclude()
public readonly csv?: Promise<FileUpload>;
}
#InputType()
#ArgsType()
export class CreateUsersInput {
#Field(() => DataObject)
public readonly data: DataObject;
}
Also, I want to mention you should not use global validation pipes (in my case they made files unreadable)
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
You could use graphql-python/gql to try to upload a file:
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(url='YOUR_URL')
client = Client(transport=transport)
query = gql('''
mutation($file: Upload!) {
createUsers(file: $file)
}
''')
with open("YOUR_FILE_PATH", "rb") as f:
params = {"file": f}
result = client.execute(
query, variable_values=params, upload_files=True
)
print(result)
If you activate logging, you can see some message exchanged between the client and the backend.

Provide explicit type for the mutation GraphQL

I'm trying to create a mutation which will accept list of products. But doing so GraphQL is throwing error for createMultipleProducts method. Not sure what is the mistake here.
import { Inject } from "#nestjs/common";
import { Args, Mutation, Query, Resolver } from "#nestjs/graphql";
import { ClientProxy } from "#nestjs/microservices";
import { ProductRequest } from "src/types/ms-product/product.request.type";
import { ProductResponse } from "src/types/ms-product/product.response.type";
#Resolver(of => ProductResponse)
export class ProductResolver {
constructor(
#Inject('SERVICE__PRODUCT') private readonly clientServiceProduct: ClientProxy
) {}
#Mutation(returns => ProductResponse)
async createProduct(#Args('data') product: ProductRequest): Promise<ProductResponse> {
const PATTERN = {cmd: 'ms-product-create'};
const PAYLOAD = product;
return this.clientServiceProduct.send(PATTERN, PAYLOAD)
.toPromise()
.then((response: ProductResponse) => {
return response;
})
.catch((error) => {
return error;
})
}
#Mutation(returns => [ProductResponse])
async createMultipleProducts(#Args('data') products: [ProductRequest]): Promise<Array<ProductResponse>> {
try {
const PROMISES = products.map(async (product: ProductRequest) => {
const PATTERN = {cmd: 'ms-product-create'};
const PAYLOAD = product;
return await this.clientServiceProduct.send(PATTERN, PAYLOAD).toPromise();
});
return await Promise.all(PROMISES);
} catch (error) {
throw new Error(error);
}
}
#Query(returns => ProductResponse)
async readProduct(#Args('data') id: string) {
return {}
}
}
I'm getting this error:
UnhandledPromiseRejectionWarning: Error: Undefined type error. Make sure you are providing an explicit type for the "createMultipleProducts" (parameter at index [0]) of the "ProductResolver" class.
There is a need to inform GraphQL the type of the arguments explicitly if it is a complex object array.
#Mutation(returns => [ProductResponse])
async createMultipleProducts(#Args({ name: 'data', type: () => [ProductRequest] }) products: ProductRequest[]): Promise<Array<ProductResponse>> {
...
}
New format 2022
#Query(returns => ProductResponse)
async readProduct(#Args('data', () => String ) id: string) {
return {}
}

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

Nested query and mutation in type-Graphql

I found a feature in graphql to write nested query and mutation, I tried it but got null. I found the best practices of building graphqL schema on Meetup HolyJs and the speaker told that one of the best ways is building "Namespaced" mutations/queries nested, in this way you can write some middlewares inside the "Namespaced" mutations/queries and for get the Child mutation you should return an empty array because if you return an empty array, Graphql understand it and go one level deep.
Please check the example code.
Example in graphql-tools
const typeDefs = gql`
type Query { ...}
type Post { ... }
type Mutation {
likePost(id: Int!): LikePostPayload
}
type LikePostPayload {
recordId: Int
record: Post
# ✨✨✨ magic – add 'query' field with 'Query' root-type
query: Query!
}
`;
const resolvers = {
Mutation: {
likePost: async (_, { id }, context) => {
const post = await context.DB.Post.find(id);
post.like();
return {
record: post,
recordId: post.id,
query: {}, // ✨✨✨ magic - just return empty Object
};
},
}
};
This is my Code
types
import { ObjectType, Field } from "type-graphql";
import { MeTypes } from "../User/Me/Me.types";
#ObjectType()
export class MeNameSpaceTypes {
#Field()
hello: string;
#Field({ nullable: true })
meCheck: MeTypes;
}
import { Resolver, Query } from "type-graphql";
import { MeNameSpaceTypes } from "./MeNamespace.types";
#Resolver()
export class MeResolver {
#Query(() => MeNameSpaceTypes)
async Me() {
const response = {
hello: "world",
meCheck:{}
};
return response;
}
}
Result of code
query {
Me{
hello
meCheck{
meHello
}
}
}
--RESULT--
{
"data": {
"Me": {
"hello": "world",
"meCheck": {
"meHello": null
}
}
}
}
I got a null instead a meHello resolver. Where am I wrong?
Namespaced mutations are against GraphQL spec as they are not guarranted to run sequentially - more info in this discussion in GitHub issue related to your problem:
https://github.com/MichalLytek/type-graphql/issues/64

Resources