I have a react-native mobile app making a call to a graphql backend. Queries are working without issue. I just coded a mutation however, and everytime I try to make a call, the client is raising this issue:
Error: Could not find a default resolver for travelPassPasswordResetEmail. Please supply a resolver for this mutation.: {"response":{"errors":[{"message":"Could not find a default resolver for travelPassPasswordResetEmail. Please supply a resolver for this mutation.","locations":[{"line":2,"column":3}],"path":["travelPassPasswordResetEmail"]}],"data":null,"status":201,"headers":{"map":{"content-type":"application/json"}}},"request":{"query":"mutation travelPassPasswordResetEmail($email: String) {\n travelPassPasswordResetEmail(email: $email) {\n message\n status\n }\n}","variables":{"email":"test#test.com"}}}
We are using graphql-codegen.
The mutation in the schema:
type Mutation {
travelPassPasswordResetEmail(email: String): SetResetPasswordMutation!
}
The operations graphql file:
mutation travelPassPasswordResetEmail($email: String) {
travelPassPasswordResetEmail(email: $email) {
message
status
}
}
In the types files we get the following generated for the mutation:
export const TravelPassPasswordResetEmail = gql`
mutation travelPassPasswordResetEmail($email: String) {
travelPassPasswordResetEmail(email: $email) {
message
status
}
}
`;
export const TravelPassPasswordResetEmailDocument = gql`
mutation travelPassPasswordResetEmail($email: String) {
travelPassPasswordResetEmail(email: $email) {
message
status
}
}
`;
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
return {
travelPassPasswordResetEmail(variables?: TravelPassPasswordResetEmailMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<TravelPassPasswordResetEmailMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<TravelPassPasswordResetEmailMutation>(TravelPassPasswordResetEmailDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'travelPassPasswordResetEmail');
}
};
}
Does anyone know why this is asking for a resolver and it cannot be found? I have tested the graphql on the backend with postman and everything is ok.
I am trying to upload multiple files with nestjs using the fastify adapter. I can do so following the tutorial in this link -article on upload
Now this does the job of file upload using fastify-multipart, but I couldnt make use of the request validations before uploading,
for example, here is my rule-file-models (which later I wanted to save to postgre)
import {IsUUID, Length, IsEnum, IsString, Matches, IsOptional} from "class-validator";
import { FileExtEnum } from "./enums/file-ext.enum";
import { Updatable } from "./updatable.model";
import {Expose, Type} from "class-transformer";
export class RuleFile {
#Expose()
#IsUUID("4", { always: true })
id: string;
#Expose()
#Length(2, 50, {
always: true,
each: true,
context: {
errorCode: "REQ-000",
message: `Filename shouldbe within 2 and can reach a max of 50 characters`,
},
})
fileNames: string[];
#Expose()
#IsEnum(FileExtEnum, { always: true, each: true })
fileExts: string[];
#IsOptional({each: true, message: 'File is corrupated'})
#Type(() => Buffer)
file: Buffer;
}
export class RuleFileDetail extends RuleFile implements Updatable {
#IsString()
#Matches(/[aA]{1}[\w]{6}/)
recUpdUser: string;
}
And I wanted to validate the multipart request and see if these are set properly.
I cannot make it to work with event subscription based approach. Here are a few things I tried - adding the interceptor, to check for the request
#Injectable()
export class FileUploadValidationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req: FastifyRequest = context.switchToHttp().getRequest();
console.log('inside interceptor', req.body);
// content type cmes with multipart/form-data;boundary----. we dont need to valdidate the boundary
// TODO: handle split errors based on semicolon
const contentType = req.headers['content-type'].split(APP_CONSTANTS.CHAR.SEMI_COLON)[0];
console.log(APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType));
const isHeaderMultipart = contentType != null?
this.headerValidation(contentType): this.throwError(contentType);
**// CANNOT check fir req.file() inside this, as it throws undefined**
return next.handle();
}
headerValidation(contentType) {
return APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType) ? true : this.throwError(contentType);
}
throwError(contentType: string) {
throw AppConfigService.getCustomError('FID-HEADERS', `Request header does not contain multipart type:
Provided incorrect type - ${contentType}`);
}
}
I wasnt able to check req.file() in the above interceptor. It throws as undefined. I tried to follow the fastify-multipart
But I wasnt able to get the request data in a prehandler as provided in the documentation for fastify-multipart
fastify.post('/', async function (req, reply) {
// process a single file
// also, consider that if you allow to upload multiple files
// you must consume all files othwise the promise will never fulfill
const data = await req.file()
data.file // stream
data.fields // other parsed parts
data.fieldname
data.filename
data.encoding
data.mimetype
// to accumulate the file in memory! Be careful!
//
// await data.toBuffer() // Buffer
//
// or
await pump(data.file, fs.createWriteStream(data.filename))
I tried getting via by registering a prehandler hook of my own like this (executed as iife)
(async function bootstrap() {
const appConfig = AppConfigService.getAppCommonConfig();
const fastifyInstance = SERVERADAPTERINSTANCE.configureFastifyServer();
// #ts-ignore
const fastifyAdapter = new FastifyAdapter(fastifyInstance);
app = await NestFactory.create<NestFastifyApplication>(
AppModule,
fastifyAdapter
).catch((err) => {
console.log("err in creating adapter", err);
process.exit(1);
});
.....
app.useGlobalPipes(
new ValidationPipe({
errorHttpStatusCode: 500,
transform: true,
validationError: {
target: true,
value: true,
},
exceptionFactory: (errors: ValidationError[]) => {
// send it to the global exception filter\
AppConfigService.validationExceptionFactory(errors);
},
}),
);
app.register(require('fastify-multipart'), {
limits: {
fieldNameSize: 100, // Max field name size in bytes
fieldSize: 1000000, // Max field value size in bytes
fields: 10, // Max number of non-file fields
fileSize: 100000000000, // For multipart forms, the max file size
files: 3, // Max number of file fields
headerPairs: 2000, // Max number of header key=>value pairs
},
});
(app.getHttpAdapter().getInstance() as FastifyInstance).addHook('onRoute', (routeOptions) => {
console.log('all urls:', routeOptions.url);
if(routeOptions.url.includes('upload')) {
// The registration actually works, but I cant use the req.file() in the prehandler
console.log('###########################');
app.getHttpAdapter().getInstance().addHook('preHandler', FilePrehandlerService.fileHandler);
}
});
SERVERADAPTERINSTANCE.configureSecurity(app);
//Connect to database
await SERVERADAPTERINSTANCE.configureDbConn(app);
app.useStaticAssets({
root: join(__dirname, "..", "public"),
prefix: "/public/",
});
app.setViewEngine({
engine: {
handlebars: require("handlebars"),
},
templates: join(__dirname, "..", "views"),
});
await app.listen(appConfig.port, appConfig.host, () => {
console.log(`Server listening on port - ${appConfig.port}`);
});
})();
Here is the prehandler,
export class FilePrehandlerService {
constructor() {}
static fileHandler = async (req, reply) => {
console.log('coming inside prehandler');
console.log('req is a multipart req',await req.file);
const data = await req.file();
console.log('data received -filename:', data.filename);
console.log('data received- fieldname:', data.fieldname);
console.log('data received- fields:', data.fields);
return;
};
}
This pattern of registring and gettin the file using preHandler works in bare fastify application. I tried it
Bare fastify server:
export class FileController {
constructor() {}
async testHandler(req: FastifyRequest, reply: FastifyReply) {
reply.send('test reading dne');
}
async fileReadHandler(req, reply: FastifyReply) {
const data = await req.file();
console.log('field val:', data.fields);
console.log('field filename:', data.filename);
console.log('field fieldname:', data.fieldname);
reply.send('done');
}
}
export const FILE_CONTROLLER_INSTANCE = new FileController();
This is my route file
const testRoute: RouteOptions<Server, IncomingMessage, ServerResponse, RouteGenericInterface, unknown> = {
method: 'GET',
url: '/test',
handler: TESTCONTROLLER_INSTANCE.testMethodRouteHandler,
};
const fileRoute: RouteOptions = {
method: 'GET',
url: '/fileTest',
preHandler: fileInterceptor,
handler: FILE_CONTROLLER_INSTANCE.testHandler,
};
const fileUploadRoute: RouteOptions = {
method: 'POST',
url: '/fileUpload',
preHandler: fileInterceptor,
handler: FILE_CONTROLLER_INSTANCE.fileReadHandler,
};
const apiRoutes = [testRoute, fileRoute, fileUploadRoute];
export default apiRoutes;
Could someone let me know the right the way to get the fieldnames , validate them befr the service being called in Nestjs
Well, I have done something like this and It works great for me. Maybe it can work for you too.
// main.ts
import multipart from "fastify-multipart";
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.register(multipart);
// upload.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
BadRequestException,
} from "#nestjs/common";
import { FastifyRequest } from "fastify";
#Injectable()
export class UploadGuard implements CanActivate {
public async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest() as FastifyRequest;
const isMultipart = req.isMultipart();
if (!isMultipart)
throw new BadRequestException("multipart/form-data expected.");
const file = await req.file();
if (!file) throw new BadRequestException("file expected");
req.incomingFile = file;
return true;
}
}
// file.decorator.ts
import { createParamDecorator, ExecutionContext } from "#nestjs/common";
import { FastifyRequest } from "fastify";
export const File = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest() as FastifyRequest;
const file = req.incomingFile;
return file
},
);
// post controller
#Post("upload")
#UseGuards(UploadGuard)
uploadFile(#File() file: Storage.MultipartFile) {
console.log(file); // logs MultipartFile from "fastify-multipart"
return "File uploaded"
}
and finally my typing file
declare global {
namespace Storage {
interface MultipartFile {
toBuffer: () => Promise<Buffer>;
file: NodeJS.ReadableStream;
filepath: string;
fieldname: string;
filename: string;
encoding: string;
mimetype: string;
fields: import("fastify-multipart").MultipartFields;
}
}
}
declare module "fastify" {
interface FastifyRequest {
incomingFile: Storage.MultipartFile;
}
}
So I found a simpler alternative. I started using fastify-multer. I used it along with this awesome lib - which made me use the multer for fastify - #webundsoehne/nest-fastify-file-upload
These are the changes I made. I registered the multer content process.
app.register(multer( {dest:path.join(process.cwd()+'/upload'),
limits:{
fields: 5, //Number of non-file fields allowed
files: 1,
fileSize: 2097152,// 2 MB,
}}).contentParser);
Then in the controller - I use it as the nestjs doc says . This actually makes fasitfy work with multer
#UseInterceptors(FileUploadValidationInterceptor, FileInterceptor('file'))
#Post('/multerSample')
async multerUploadFiles(#UploadedFile() file, #Body() ruleFileCreate: RuleFileCreate) {
console.log('data sent', ruleFileCreate);
console.log(file);
// getting the original name of the file - no matter what
ruleFileCreate.originalName = file.originalname;
return await this.fileService.fileUpload(file.buffer, ruleFileCreate);
}
BONUS - storing the file in local and storing it in DB - Please refer
github link
I am using Typescript, Express, TypeORM, GraphQL and TypeGraphQL to build a small app that allows the user to login.
However, when I hit my test query bye on the GraphQL playground, I get:
Cannot read property 'context' of undefined
"TypeError: Cannot read property 'context' of undefined",
" at new exports.isAuth // isAuth is a JS file I wrote
MyContext.js
import { Request, Response } from "express";
export interface MyContext {
req: Request;
res: Response;
payload?: { userId: string };
}
isAuth.js
import { MiddlewareFn } from "type-graphql";
import { verify } from "jsonwebtoken";
import { MyContext } from "./MyContext";
export const isAuth: MiddlewareFn<MyContext> = ({ context }, next) => {
const authorization = context.req.headers["authorization"];
if (!authorization) {
throw new Error("not authorized");
}
...
UserResolver
#Query(() => String)
#UseMiddleware(isAuth)
bye(#Ctx() { payload }: MyContext) {
console.log(payload);
return `your user id is: ${payload!.userId}`;
}
I am not sure why the context is undefinied in the file isAuth.js
SOLVED thanks to: https://github.com/MichalLytek/type-graphql/issues/433
1) Go into ./tsconfig.json
2) Change "target": "es5" to "target": "es6"
Im working with apollo-server, everything works as expetected but the mutation arguments are undefined when the mutation is called from the frontend.
const express = require('express');
const morgan = require('morgan');
const { ApolloServer, gql } = require('apollo-server-express');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
const typeDefs = gql`
type msgFields {
email: String!
textarea: String!
createdAt: String!
}
input MsgFieldsInput {
email: String!
textarea: String!
createdAt: String!
}
type Query {
formContact: msgFields!
}
type Mutation {
createMsg(email: String!, textarea: String!, createdAt: String!): String!
}
`;
const resolvers = {
Query: {
formContact: () => {
return {
email: 'test#mail.com',
textarea: 'checking Checking checking Checking checking Checking'
}
}
},
Mutation: {
createMsg: (args) => {
console.log(args); // => undefined here
return 'Worked';
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
});
app.use(morgan('dev'));
server.applyMiddleware({app})
mongoose.connect(process.env.MONGO_URL, { useNewUrlParser: true })
.then(() => {
app.listen({port: 4000}, () => {
console.log(`Server and DB ready at http://localhost:4000${server.graphqlPath}`)
});
})
.catch(err => {
throw err;
})
This is what i send from /graphql
mutation {
createMsg(email: "test#mail.com" textarea: "testing textarea" createdAt: "19-05-2018")
}
The resolver signature is as follows: (parent, args, context, info) where:
parent: The object that contains the result returned from the resolver on the parent field, or, in the case of a top-level Query field, the rootValue passed from the server configuration. This argument enables the nested nature of GraphQL queries.
args: An object with the arguments passed into the field in the query. For example, if the field was called with query{ key(arg: "you meant") }, the args object would be: { "arg": "you meant" }.
context: This is an object shared by all resolvers in a particular query, and is used to contain per-request state, including authentication information, dataloader instances, and anything else that should be taken into account when resolving the query. Read this section for an explanation of when and how to use context.
info: This argument contains information about the execution state of the query, including the field name, path to the field from the root, and more. It's only documented in the GraphQL.js source code, but is extended with additional functionality by other modules, like apollo-cache-control.
The arguments are passed to the resolver as the second parameter, not the first. See the docs for additional details.
I'm using schema directives for authorization on fields. Apollo server calls the directives after the resolvers have returned. Because of this the directives don't have access to the output so when authorization fails I can't include relevant information for the user without a convoluted workaround throwing errors that ends up always returning the error data whether the query requests them or not.
I'm hoping someone understands the internals of Apollo better than I and can point out where I can insert the proper information from directives so I don't have to break the standard functionality of GraphQL.
I tried including my output in the context, but that doesn't work despite the directive having access since the data has already been returned from the resolvers and the context version isn't needed after that.
As of right now I throw a custom error in the directive with a code DIRECTIVE_ERROR and include the message I want to return to the user. In the formatResponse function I look for directive errors and filter the errors array by transferring them into data's internal errors array. I know formatResponse is not meant for modifying the content of the data, but as far as I know this is the only place left where I can access what I need. Also frustrating is the error objects within the response don't include all of the fields from the error.
type User implements Node {
id: ID!
email: String #requireRole(requires: "error")
}
type UserError implements Error {
path: [String!]!
message: String!
}
type UserPayload implements Payload {
isSuccess: Boolean!
errors: [UserError]
data: User
}
type UserOutput implements Output {
isSuccess: Boolean!
payload: [UserPayload]
}
/**
* All output responses should be of format:
* {
* isSuccess: Boolean
* payload: {
* isSuccess: Boolean
* errors: {
* path: [String]
* message: String
* }
* data: [{Any}]
* }
* }
*/
const formatResponse = response => {
if (response.errors) {
response.errors = response.errors.filter(error => {
// if error is from a directive, extract into errors
if (error.extensions.code === "DIRECTIVE_ERROR") {
const path = error.path;
const resolverKey = path[0];
const payloadIndex = path[2];
// protect from null
if (response.data[resolverKey] == null) {
response.data[resolverKey] = {
isSuccess: false,
payload: [{ isSuccess: false, errors: [], data: null }]
};
} else if (
response.data[resolverKey].payload[payloadIndex].errors == null
) {
response.data[resolverKey].payload[payloadIndex].errors = [];
}
// push error into data errors array
response.data[resolverKey].payload[payloadIndex].errors.push({
path: [path[path.length - 1]],
message: error.message,
__typename: "DirectiveError"
});
} else {
return error;
}
});
if (response.errors.length === 0) {
return { data: response.data };
}
}
return response;
};
My understanding of the order of operations in Apollo is:
resolvers return data
data filtered based on query parameters?
directives are called on the object/field where applied
data filtered based on query parameters?
formatResponse has opportunity to modify output
formatError has opportunity to modify errors
return to client
What I'd like is to not have to throw errors in the directives in order to create info to pass to the user by extracting it in formatResponse. The expected result is for the client to receive only the fields it requests, but the current method breaks that and returns the data errors and all fields whether or not the client requests them.
You can inject it using destruct:
const { SchemaDirectiveVisitor } = require("apollo-server-express");
const { defaultFieldResolver } = require("graphql");
const _ = require("lodash");
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (parent, args, context, info) {
// You could e.g get something from the header
//
// The verification below its necessary because
// my application runs locally and on Serverless
const authorization = _.has(context, "req")
? context.req.headers.authorization
: context.headers.Authorization;
return resolve.apply(this, [
parent,
args,
{
...context,
user: { authorization, name: "", id: "" }
},
info,
]);
};
}
}
Then on your resolver, you can access it through context.user.