type-graphql icon indicating copy to clipboard operation
type-graphql copied to clipboard

File upload

Open MichalLytek opened this issue 6 years ago • 54 comments

Integration with https://github.com/jaydenseric/apollo-upload-server

MichalLytek avatar Mar 02 '18 11:03 MichalLytek

Hi,

Any news about this feature ?

I tried using https://github.com/jaydenseric/apollo-upload-server, i configured a upload resolver like this :

import {GraphQLUpload} from "apollo-upload-server";
...
@Mutation(returns => Media)
async upload(@Arg('file') file: GraphQLUpload): Promise<Media> {
    // process upload
    const media = this.repository.create({});
    return await this.repository.save(media);
}

But i've got this error : UnhandledPromiseRejectionWarning: Error: You need to provide explicit type for UploadResolver#upload parameter #0

How to fix this error ? Thanks

daviddlv avatar Jul 24 '18 20:07 daviddlv

https://19majkel94.github.io/type-graphql/docs/scalars.html#custom-scalars

You need to provide the scalar type to the @Arg decorator due to limited TypeScript reflection capability. And the TS type is Upload object, not the GQL scalar instance.

async upload(@Arg('file', type => GraphQLUpload) file: Upload): Promise<Media> {

MichalLytek avatar Jul 25 '18 07:07 MichalLytek

Thanks @19majkel94 it works

daviddlv avatar Jul 25 '18 20:07 daviddlv

@19majkel94 where do you import TS type Upload from?

danpaugo avatar Aug 04 '18 13:08 danpaugo

@danpaugo I've found some types definition in github issue: https://github.com/jaydenseric/apollo-upload-server/issues/70 And I've enhanced them with Upload interface:

declare module "apollo-upload-server" {
  import { GraphQLScalarType } from "graphql";
  import { RequestHandler } from "express";
  import { Readable } from "stream";

  export interface UploadMiddlewareOptions {
    maxFieldSize?: number;
    maxFileSize?: number;
    maxFiles?: number;
  }

  export interface Upload {
    stream: Readable;
    filename: string;
    mimetype: string;
    encoding: string;
  }

  export const GraphQLUpload: GraphQLScalarType;
  export function apolloUploadExpress(
    options?: UploadMiddlewareOptions,
  ): RequestHandler;
}

I will try to upload this definition to DefinitelyTyped repo to make @types work.

MichalLytek avatar Aug 05 '18 19:08 MichalLytek

Hi , i still experience troubles with it the package now renamed graphql-upload should still be working but i keep having

the cannot find declaration types

issue despite the fact that i used the example shown above...it is like tsnode completely ignores the declaration files.

zjjt avatar Jan 14 '19 15:01 zjjt

Here is the error i keep having. I use apollo-server 2.3.1 which comes embedded with graphql-upload 5new name for apollo-server-upload). Here is what i am getting as errors:

node:60159) UnhandledPromiseRejectionWarning: TSError: ⨯ Unable to compile TypeScript: src/GraphqlExpress/resolvers/FileUpload_resolver.ts(3,29): error TS7016: Could not find a declaration file for module 'graphql-upload'. '/Users/macbook/Desktop/Z-projects/GESSMS/node_modules/graphql-upload/lib/index.js' implicitly has an 'any' type. Try npm install @types/graphql-upload if it exists or add a new declaration (.d.ts) file containing declare module 'graphql-upload';

at createTSError (/Users/macbook/Desktop/Z-projects/GESSMS/node_modules/ts-node/src/index.ts:261:12)
at getOutput (/Users/macbook/Desktop/Z-projects/GESSMS/node_modules/ts-node/src/index.ts:367:40)
at Object.compile (/Users/macbook/Desktop/Z-projects/GESSMS/node_modules/ts-node/src/index.ts:558:11)
at Module.m._compile (/Users/macbook/Desktop/Z-projects/GESSMS/node_modules/ts-node/src/index.ts:439:43)
at Module._extensions..js (module.js:663:10)
at Object.require.extensions.(anonymous function) [as .ts] (/Users/macbook/Desktop/Z-projects/GESSMS/node_modules/ts-node/src/index.ts:442:12)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Module.require (module.js:596:17)

(node:60159) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 4) (node:60159) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate theNode.js process with a non-zero exit code.

Can you help me please?

zjjt avatar Jan 14 '19 15:01 zjjt

@zjjt

it is like tsnode completely ignores the declaration files.

Can you help me please?

It's not a problem with TypeGraphQL itself. Please read the docs for ts-node or open an issue in ts-node repository.

MichalLytek avatar Jan 14 '19 15:01 MichalLytek

@19majkel94 thank you for replying.

zjjt avatar Jan 14 '19 16:01 zjjt

@daviddlv have you a example? for upload with scalar on type-graphql?

johinsDev avatar Jan 29 '19 16:01 johinsDev

@johinsDev , here an example of my resolver : https://gist.github.com/daviddlv/0259ce8b64a3e23611a96b0c52c27a27

daviddlv avatar Jan 29 '19 19:01 daviddlv

@johinsDev , here an example of my resolver : https://gist.github.com/daviddlv/0259ce8b64a3e23611a96b0c52c27a27

thanks, its cool. but what have you FileInput?

johinsDev avatar Jan 30 '19 17:01 johinsDev

I put the FileInput file in the gist

daviddlv avatar Jan 30 '19 18:01 daviddlv

@daviddlv @19majkel94

I tried to use your gist, but I'm getting (node:18235) UnhandledPromiseRejectionWarning: Error: Cannot determine GraphQL input type for stream.

import { Stream } from 'stream';
import { Field, InputType, Int } from 'type-graphql';

@InputType()
export class FileInput {
  @Field(type => Stream) // <<== the problem
  stream: Stream;

  @Field() filename: string;

  @Field() mimetype: string;

  @Field() encoding: string;
}

Here's the js output:

const type_graphql_1 = require("type-graphql");
const stream_1 = require("stream");
let FileInput = class FileInput {
};
__decorate([
    type_graphql_1.Field(type => stream_1.Stream),
    __metadata("design:type", stream_1.Stream)
], FileInput.prototype, "stream", void 0);
__decorate([
    type_graphql_1.Field(),
    __metadata("design:type", String)
], FileInput.prototype, "filename", void 0);
__decorate([
    type_graphql_1.Field(),
    __metadata("design:type", String)
], FileInput.prototype, "mimetype", void 0);
__decorate([
    type_graphql_1.Field(),
    __metadata("design:type", String)
], FileInput.prototype, "encoding", void 0);
FileInput = __decorate([
    type_graphql_1.InputType()
], FileInput);
exports.FileInput = FileInput;

laukaichung avatar Apr 18 '19 23:04 laukaichung

@laukaichung All you need is:

@Arg('file', type => GraphQLUpload)
file: FileInput

the FileInput might be just an TS interface, no need for an @InputType because it won't work.

MichalLytek avatar Apr 19 '19 09:04 MichalLytek

hi @19majkel94 , i have issue,, with arg type of array, here is example:

@ArgsType()
export class XArg {
  @Field()
  name: string;

  @Field({ nullable: true })
  value?: string;
}

// resolver

@Mutation(returns => Response)
  async xCreate(
    @Ctx() context: Context,
    @Arg("xarr") xarr: Array<XArg>
  ): Promise<ResponseInterface> {}

Error: You need to provide explicit type for XResolver#xCreate parameter #1

thanks before

sgtkuncoro avatar May 31 '19 06:05 sgtkuncoro

@sgtkuncoro

provide explicit type

@Arg("xarr", type => [XArg]) xarr

MichalLytek avatar May 31 '19 08:05 MichalLytek

Working solution for me:

import { Resolver, Mutation, Arg } from 'type-graphql'
import { GraphQLUpload, FileUpload } from 'graphql-upload'
import os from 'os'
import { createWriteStream } from 'fs'
import path from 'path'

@Resolver()
export default class SharedResolver {
  @Mutation(() => ImageResource)
  async uploadImage(
    @Arg('file', () => GraphQLUpload)
    file: FileUpload
  ): Promise<ImageResource> {
    const { createReadStream, filename } = await file

    const destinationPath = path.join(os.tmpdir(), filename)

    const url = await new Promise((res, rej) =>
      createReadStream()
        .pipe(createWriteStream(destinationPath))
        .on('error', rej)
        .on('finish', () => {
          // Do your custom business logic

          // Delete the tmp file uploaded
          unlink(destinationPath, () => {
            res('your image url..')
          })
        })
    )

    return {
      url
    }
  }
}


andfk avatar Sep 11 '19 12:09 andfk

Thank you for proposed solution @andfk @daviddlv . It seems to be working until I try to upload more than 10 files at once. In that case I am getting this warning:

MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 exit listeners added. Use emitter.setMaxListeners() to increase limit
demo:  |     at _addListener (events.js:280:19)
demo:  |     at process.addListener (events.js:297:10)
demo:  |     at _fs.default.open (***/node_modules/fs-packages/api/capacitor/lib/index.js:140:17)
demo:  |     at FSReqWrap.oncomplete (fs.js:135:15)

My resolver looks like this:

const fs = require('fs');
import { UploadResult } from '../graphql/models/file';
import { GraphQLUpload, FileUpload } from 'graphql-upload';
import { Resolver, Mutation, Arg } from 'type-graphql';
import isArray from 'lodash/isArray';

@Resolver()
class FileResolver {
  @Mutation(() => UploadResult)
  async fileUpload(@Arg('fileInput', () => GraphQLUpload) fileInput: FileUpload): Promise<UploadResult> {
    let readableStreams = [];
    if (isArray(fileInput)) {
      readableStreams = await Promise.all(fileInput);
    } else {
      readableStreams[0] = await fileInput;
    }
    const pipedStreams = readableStreams.map((readStreamInstance) => {
      const { filename, createReadStream } = readStreamInstance;
      const writableStream = fs.createWriteStream(`./${filename}`, { autoClose: true });
      return new Promise<string>((resolve, reject) => {
        createReadStream()
          .pipe(writableStream)
          .on('error', (error: any) => {
            reject(error);
          })
          .on('finish', () => {
            resolve(filename);
          });
      })
    });
    const results = await Promise.all(pipedStreams);
    return {
      uploaded: results
    }
  }
}

The warning gets fired even if I comment-out the whole resolver-function body and just return some random string array. So I suspect that the error is not coming from the resolver itself. It's rather coming from some underlying implementation.

Does anybody has similar problem?

jordan-jarolim avatar Oct 04 '19 13:10 jordan-jarolim

So the warning is coming from eventemitter which is used by nodejs streams. You can bypass the warning by require('events').EventEmitter.defaultMaxListeners = uploadMultipleMaxFiles; I wanted to be sure that I don't have any memory leakage, so I wrote a "test" with multiple uploads - https://gist.github.com/jordan-jarolim/4d374c1b864de4c6321f611f748dc5bd and check memory management using --expose-gc --inspect=9222 flags for node process and chrome://inspect tool.

jordan-jarolim avatar Oct 08 '19 11:10 jordan-jarolim

Would love to use this, but when I tried I got this error message: Error: This implementation of ApolloServer does not support file uploads because the environment cannot accept multi-part forms

This happens when I try to set the uploads key on the new ApolloServer I'm creating. Without it, I get a invalid json error. Any ideas?

alondahari avatar Jan 19 '20 06:01 alondahari

Apollo Server version? Apolo + express, + koa ...?

MOTORIST avatar Jan 19 '20 09:01 MOTORIST

Hey guys, @andfk implementation does not work for me. It fails with this message: got invalid value {}; Expected type Upload. Upload value invalid.

This is what my resolver looks like:

import { Resolver, Mutation, Arg, Int } from 'type-graphql';
import { GraphQLUpload, FileUpload } from 'graphql-upload';

@Resolver()
export class UploadResolver {
  @Mutation(_ => Int)
  singleUpload(@Arg('file', () => GraphQLUpload) file: FileUpload) {
    console.log(file);
    return 3;
  }
}

This is the query I execute (I have also tried in the app with same result):

curl localhost:3333/graphql \
  -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) }", "variables": { "file": null } }' \
  -F map='{ "0": ["variables.file"] }' \
  -F [email protected]
{
"errors": [
{
"message":"Variable \"$file\" got invalid value {}; Expected type Upload. Upload value invalid.",
"locations":[{"line":1,"column":11}],
"extensions":{"code":"INTERNAL_SERVER_ERROR",
"exception": {
  "message":"Upload value invalid.",
  "stacktrace":[
    "GraphQLError: Upload value invalid.",
    "    at GraphQLScalarType.parseValue (/project/node_modules/graphql-upload/lib/GraphQLUpload.js:66:11)",
    "    at coerceInputValueImpl 
[-- snip --]

The relevant dependencies as follows:

    "apollo-server-express": "^2.10.0",
    "express": "4.17.1",
    "graphql": "^14.6.0",
    "graphql-tag": "^2.10.3",
    "graphql-upload": "^10.0.0",

Am I missing something?

FWIW, here is the React implementation of upload, that returns the same error:

import React from 'react';
import { useApolloClient, useMutation } from '@apollo/react-hooks';
import gql from 'graphql-tag';

const SINGLE_UPLOAD_MUTATION = gql`
  mutation singleUpload($file: Upload!) {
    singleUpload(file: $file)
  }
`;

export const UploadFile = () => {
  const [uploadFileMutation] = useMutation(SINGLE_UPLOAD_MUTATION);
  const apolloClient = useApolloClient();

  const onChange = ({
    target: {
      validity,
      files: [file]
    }
  }) =>
    validity.valid &&
    uploadFileMutation({ variables: { file } }).then(() => {
      apolloClient.resetStore();
    });

  return <input type="file" required onChange={onChange} />;
};

naishe avatar Feb 28 '20 05:02 naishe

@naishe have you added the express middleware?

MichalLytek avatar Feb 28 '20 08:02 MichalLytek

Yes, I like this:

const app = express();
// -- snip --
apolloServer.applyMiddleware({ app });

I think I figured the problem. import { GraphQLUpload, FileUpload } from 'graphql-upload'; was the issue. It seems the graphql-upload library is not needed with the latest Apollo Server. The following code works:

import { Resolver, Mutation, Arg, Int } from 'type-graphql';
import { GraphQLUpload } from 'apollo-server-express';

export interface FileUpload {
  filename: string;
  mimetype: string;
  encoding: string;
  createReadStream(): ReadStream;
}

@Resolver()
export class UploadResolver {
  @Mutation(_ => Int)
  async singleUpload(@Arg('file', () => GraphQLUpload) file: FileUpload) {
    console.log(file);
    return 4; // ideally, you'd return something sensible like an URL 
}

naishe avatar Feb 28 '20 08:02 naishe

Yes, apollo server has this built-in. For other cases, you need to apply the upload middleware:

express()
  .use(
    '/graphql',
    graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }),
    graphqlHTTP({ schema })
  )
  .listen(3000)

MichalLytek avatar Feb 28 '20 10:02 MichalLytek

apollo-server-express use the version 8 of graphql-upload and it not support node version 13 throwing this error Maximum call stack size exceeded this error solved in graphql-upload in this issues but when we install the new version of graphql-upload the server response with throwing got invalid value {}; Expected type Upload. Upload value invalid.

hemedani avatar Feb 28 '20 10:02 hemedani

when i host graphql-upload on monorepo for using the right version of that type-graphql throwing this error : Cannot determine GraphQL input type for image

hemedani avatar Feb 28 '20 10:02 hemedani

I solved this with disabling apollo-server upload property :

    const apolloServer = new ApolloServer({
        schema,
        context: ({ req }) => ({ req }),
        introspection: true,
        uploads: false // disable apollo upload property
    });

install the new version of graphql-upload : yarn add graphql-upload

Setup the graphql-upload middleware.

const app = Express();
app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));
apolloServer.applyMiddleware({
        app
    });

and then setup the upload mutation :

import { Resolver, Mutation, Arg, Int } from 'type-graphql';
import { GraphQLUpload, FileUpload } from "graphql-upload";
import { createWriteStream } from "fs";

@Resolver()
export class UploadResolver {
  @Mutation(_ => Promise<Boolean>)
  async singleUpload(@Arg('file', () => GraphQLUpload) file: FileUpload) {
    const { createReadStream, filename } = await file;
    const writableStream = createWriteStream(
            `${__dirname}/../../../files/images/${filename}`,
            { autoClose: true }
        );
    return new Promise((res, rej) => {
            createReadStream()
                .pipe(writableStream)
                .on("finish", () => res(true))
                .on("error", () => rej(false));
        });
}

hemedani avatar Feb 28 '20 11:02 hemedani

I solved this with disabling apollo-server upload property :

    const apolloServer = new ApolloServer({
        schema,
        context: ({ req }) => ({ req }),
        introspection: true,
        uploads: false // disable apollo upload property
    });

install the new version of graphql-upload : yarn add graphql-upload

Setup the graphql-upload middleware.

const app = Express();
app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));
apolloServer.applyMiddleware({
        app
    });

and then setup the upload mutation :

import { Resolver, Mutation, Arg, Int } from 'type-graphql';
import { GraphQLUpload, FileUpload } from "graphql-upload";
import { createWriteStream } from "fs";

@Resolver()
export class UploadResolver {
  @Mutation(_ => Promise<Boolean>)
  async singleUpload(@Arg('file', () => GraphQLUpload) file: FileUpload) {
    const { createReadStream, filename } = await file;
    const writableStream = createWriteStream(
            `${__dirname}/../../../files/images/${filename}`,
            { autoClose: true }
        );
    return new Promise((res, rej) => {
            createReadStream()
                .pipe(writableStream)
                .on("finish", () => res(true))
                .on("error", () => rej(false));
        });
}

This solution worked for me, make sure you guys have created the folder where the files will be uploaded, otherwise you'll get an error

andersoncscz avatar Feb 29 '20 17:02 andersoncscz