prisma-labs / graphql-framework-experiment

Code-First Type-Safe GraphQL Framework
https://nexusjs.org
MIT License
674 stars 66 forks source link

File uploads #844

Open jasonkuhrt opened 4 years ago

jasonkuhrt commented 4 years ago

Perceived Problem

Ideas / Proposed Solution(s)

References

leungandrew commented 4 years ago

Until there's some built-in solution, for those looking to incorporate https://github.com/jaydenseric/graphql-upload, they can do so by the following:

import { server, schema } from 'nexus'
import { GraphQLUpload, graphqlUploadExpress } from 'graphql-upload'

server.express.use(
  '/graphql',
  graphqlUploadExpress(),
)

schema.scalarType(GraphQLUpload)

And to test it out, use a GraphQL Client like Altair that supports file uploads and create a mutation like so:

import { schema } from 'nexus'
schema.mutationType({
  definition(t) {
    t.field('uploadFile', {
      args: {
        file: schema.arg({ type: 'Upload', required: true}),
      },
      type: 'Boolean',
      resolve: async (root, { file }, ctx) => {
        const { filename } = await file
        console.log(filename)
        return true
      },
    })
  },
})
turboxx commented 4 years ago

@leungandrew Does this solution still work on ^0.26.0-next.14 I have the exact implementation and receiving error nexus:server:graphql Missing multipart field ‘operations’

leungandrew commented 4 years ago

Yeah, I just attempted using ^0.26.0-next.14 and seems to have broke the workaround. I don't have a solution around this. It appears https://github.com/graphql-nexus/nexus/releases/tag/0.26.0-next.8 introduced this error. It works from .7 and below. Maybe you can dive a bit deeper into what got introduced in .8 that broke this?

bkrausz commented 4 years ago

0.26.0 natively supports file uploads, as they've switched to apollo-server. You can now do this:

import { GraphQLUpload } from 'apollo-server-core';
import type { FileUpload } from 'graphql-upload';

export type UploadRoot = Promise<FileUpload>;

schema.scalarType({
  // Why we need the bang: https://github.com/apollographql/apollo-server/blob/570f548b88750a06fbf5f67a4abe78fb0f870ccd/packages/apollo-server-core/src/index.ts#L49-L56
  ...GraphQLUpload!,
  rootTyping: 'UploadRoot',
})

Unfortunately, the version of graphql-upload that apollo-server uses causes crashes on Node 13+. I used resolutions for forcibly bump it, as the API is compatible and there's no way to turn that version off to inject your own (Nexus would need to thread through a setting).

Maetes commented 4 years ago

@bkrausz maybe do you have a full example also showing the resolver implementation for a storange in the file system? would really appreciate it. Thanks anyway for this mention! Do you also know the start date of 0.26.0? Will it also fix the Error code 500 bugs? Will we still be able to use express middlewares?

bkrausz commented 4 years ago

a full example also showing the resolver implementation for a storange in the file system?

async resolve(_root, { file }, ctx) {
  const { filename, mimetype, createReadStream } = await file as UploadRoot,
  // Do something with filename, call createReadStream() to get a stream
  // ...
}

You can lookup how to use Node streams to do what you want with a file (I pipe it into an S3 upload). StackOverflow has examples.

Do you also know the start date of 0.26.0?

No clue, I don't work directly on Nexus, just a user

Will it also fix the Error code 500 bugs?

I don't know what 500 bugs you're referring to...

Will we still be able to use express middlewares?

Yes, server.express.use is still available.

Maetes commented 4 years ago

Thanks alot @bkrausz ! Regarding the 500'er bugs: out of the box when u rise an errror on the backend it will be send to the frontend as a 500er error. The common way of sending errors with graphql however is to rise an error but the server should send a 200er to the frontend. I managed this issue with a custom middleware right now. Switching to apollo server could fix this issue too.

I don't want to pollute this upload thread with it but i hope @jasonkuhrt could reveal this to us.

macrozone commented 4 years ago

@leungandrew Does this solution still work on ^0.26.0-next.14 I have the exact implementation and receiving error nexus:server:graphql Missing multipart field ‘operations’

i also got this error. Could you solve it?

i am using nexus 0.26.1 and @bkrausz 's code snippet

bkrausz commented 4 years ago

Are you adding GQL upload middleware yourself? You shouldn’t because then you’d have 2 copies running at the same time. My 2 snippets are the only code I’m using to enable uploads.

macrozone commented 4 years ago

@bkrausz no, i just use nexus inside nextjs (according to the example), maybe the middleware is missing in that setup?

bkrausz commented 4 years ago

@macrozone I'm quite unfamiliar with the nextjs side of Nexus, it's possible.

macrozone commented 4 years ago

how nexus uses apollo server in case of nextjs? It uses

const app = require("nexus").default;

export default app.server.handlers.graphql;

so this handler is responsible for graphql and forwards it to apollo-server. Does this use any middleware? Can i wrap it with the middleware?

macrozone commented 4 years ago

update: setting

export const config = {
  api: {
    bodyParser: false
  }
}

on the nextjs api route does not trigger the Missing multipart field ‘operations’-error, but the resolver will not be called and the request hangs.

Edit: it then successfuly reads the file, but after that its passed to the graphql part of the multipart request. There it tries to parse the query from the body and this hangs. It will eventuelly passed torawBody and then hangs, similar as described here: https://github.com/stream-utils/raw-body/issues/57

it seems that its then already parsed, but nexus does not handle this correctly

macrozone commented 4 years ago

ok i found the problems:

first, this is required i the api rout:

export const config = {
  api: {
    bodyParser: false
  }
}

second, the uploadMiddleware is indeed called and sets req.filePayload, but it was forgotten to use that in the serverless version.

Similarly to https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-micro/src/microApollo.ts#L46, this has to be used in graphqlHandler. Something like that:

   const query = req.method === 'POST' ? (req.filePayload ? {right: req.filePayload} : await parse_body_1.parseBody(req)) : parse_body_1.parseQuery(req);

I will send in a PR later that fixes the bug

alexichepura commented 4 years ago

nexus 0.26 native uploads work for me well Examples from bkrausz are very helpful https://github.com/graphql-nexus/nexus/issues/844#issuecomment-666699822 https://github.com/graphql-nexus/nexus/issues/844#issuecomment-667588707

I also had to fixate graphql-upload version that have upgraded fs-capacitor

  "resolutions": {
    "graphql-upload": "^11.0.0",
  }

remove multipart parser if any used app.server.express.use(multipart) app.server.express.use(bodyParser.json()) to eliminate this error nexus:server:graphql Missing multipart field ‘operations’

Thank you all! =)

TommyCoding commented 4 years ago

i am currently trying to dig in to nexus 0.26.1 and want to use file uploads but I couldn´t stick the right parts together using the comments and code from here.

There is also a small sentence in docs mentioning the Upload scalar but I could not find out on how to use it. https://nexusjs.org/components-standalone/schema/api/api-scalarType#example-of-upload-scalar

@alexichepura: May I ask you to give an example of code (or the steps for implementation) you`ve used for the current version if possible please ?

alexichepura commented 4 years ago

@TommyCoding I didn't use docs, I used exact code from the bkrausz' comment https://github.com/graphql-nexus/nexus/issues/844#issuecomment-666699822

TommyCoding commented 4 years ago

i have used it too but I don`t know how to use that scalar for a mutation?

schema.extendType({
  type: 'Mutation',  
  definition(t) {
    t.field('upload', {
      type: 'UploadTask',
      args: {
        // <-- how to use upload as arg?
        // or is there antoher way?
      },
      resolve(_root, args, ctx) {
        // ...
  }
  })
})

used the code from https://github.com/graphql-nexus/nexus/issues/844#issuecomment-666699822 before using the code above.

alexichepura commented 4 years ago

First I would search for an "Upload" scalar in schema.

"""The `Upload` scalar type represents a file upload."""
scalar Upload

Then it's possible to use it as an argument.

  t.field("upload", {
    type: "String",
    args: {
      image: arg({ type: "Upload", required: true }),
    },
    resolve: async (_parent, args, _ctx) => await gcp_upload(args.image),
  })

Note: I send image variable using this spec https://github.com/jaydenseric/graphql-multipart-request-spec

steinathan commented 4 years ago

@alexichepura I used this but i encountered a problem with the underlying dependency that nexus is using https://github.com/graphql-nexus/nexus/issues/1413 how did you overcome the bug?