Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

[Proposal] File Uploads #65

Closed
jangerhofer opened this issue Sep 9, 2016 · 64 comments
Closed

[Proposal] File Uploads #65

jangerhofer opened this issue Sep 9, 2016 · 64 comments

Comments

@jangerhofer
Copy link

jangerhofer commented Sep 9, 2016

After toying with the Express-GraphQL file upload demo, I have been thinking about how to bring file uploads inside GraphQL mutations to Apollo.

Here is the rundown of what I've learned and the best solution I have formulated:

  • Canonical file uploads happen via a mutipart/form-data POST request, whereas Apollo Client interfaces with the Server exclusively with application/json requests at present.

  • There are other ways to transmit a file from browser to server, but they are often flimsy and poor solutions.

  • The ideal solution delivers both a GraphQL mutation and the uploaded file(s) to the server in such a way that the schema resolvers have access to the files, such that custom logic can test, modify, and/or store the uploads. The best way to ensure both mutation string and file(s) reach the server at the same time is to merge them into the same request.

  • Thus, I think that Apollo Client needs to either

    • gain a new method, e.g. mutateWithFiles( "gqlQueryString", { apolloOptions }, [ files ] ) or
    • include a new option, e.g. uploadFiles : [ ... ]

    which, when used, sends a multipart/form-data POST to the specified Apollo Server endpoint. The request would be identical to the normal request apart from the inclusion of a new files field or similar which contains the uploading files. I think I would lean towards the clarity of a new method on account of readability.

  • Apollo Server will then need to check each request for its content-type; if it is application/json, then no upload-specific logic happens and the mutation continues as usual. If, however, the POST is of content-type multipart/form-data, the server will append the files in memory to the root query object. The mutation then continues as usual.

    • This way, the files are available to the resolvers via the first root argument and subsequently to connectors if need-be.

Please chime in if you have any thoughts. I'd especially like for members of the Apollo team to offer their opinions. If we get to a design that satisfies the dev team, I am happy to work alongside any other interested parties to create a PR!

@stubailo
Copy link
Contributor

stubailo commented Sep 9, 2016

That sounds pretty cool, I'd be interested to hear more about how people do this with express-graphql, and maybe also Relay today. The solution proposed sounds reasonable, but I want to see if it aligns with what other people have done.

@HriBB
Copy link

HriBB commented Sep 9, 2016

Using Koa v2 on the server. I have a custom /upload endpoint with async-busboy that handles file uploads for the entire app. For example I send type SalonPicture, list of files and all extra parameters. After a successful upload, I fire refresh-salon-pictures with fbemitter to refetch data on all components that listen to this specific event. It's not so bad, but I would prefer to have this logic in graphql. Unless someone can convince me otherwise.

I really like what @jangerhofer is proposing. Maybe we can support file uploads without even touching the core. We can create custom middleware for each server integration. On express and koa it should be possible, not sure about hapi.

As for the client. Can we somehow pull it of with middleware or do we need a custom NetworkInterface?

@stubailo
Copy link
Contributor

stubailo commented Sep 9, 2016

Hmmm, OK, this is starting to sound pretty cool. What about this:

  1. We pass files via variables to the mutation
  2. The network interface looks through the variables, and looks for any files
  3. The files are moved to the multipart request body, and the variable is replaced with some kind of reference
  4. There's a server middleware that pulls files off the body, and puts them back into the variables in the right location
  5. In your mutation arguments, you get a file object

Alternative:

  1. (1-3 from above)
  2. There's a server middleware that pulls files off the body, uploads them to a file storage service, and replaces the variables with a reference to that file on S3 or wherever
  3. In your mutation arguments, you get a reference to a file that has been uploaded to S3 already

Other options?

Still interested to see if this server component will be compatible with Relay - someone should go look at how that works.

@HriBB
Copy link

HriBB commented Sep 10, 2016

I did some analysis. Most of the Relay examples use express-graphql. From the docs:

GraphQL will first look for each parameter in the URL's query-string
If not found in the query-string, it will look in the POST request body.
If a previous middleware has already parsed the POST body, the request.body value will be used. Use multer or a similar middleware to add support for multipart/form-data content, which may be useful for GraphQL mutations involving uploading files.

If that is what you mean?

@jangerhofer
Copy link
Author

jangerhofer commented Sep 10, 2016

Two thoughts, @stubailo; first...

uploads them to a file storage service, and replaces the variables with a reference

Perhaps I am alone in this, but I strongly prefer to avoid prescriptive solutions whenever possible. It's a totally reasonable proposition to have integrations with different services, but I would want to see that in an add-on package rather than within the base functionality.

Second, I'm not sure I follow the requests that are made in steps 2+3. Are you thinking of using one request of type app/json and a subsequent POST of type multipart, where the latter holds the files themselves? If so, how do you tie the two requests together on the server, given the unknowns in timing?

At any rate, if it is possible to determine which variables are of the file-type, then that's an even more elegant solution from the user's perspective!

@sandervanhooft
Copy link

@jangerhofer You're not alone in this. That's why I prefer @stubailo's first suggestion:

In your mutation arguments, you get a file object

This way the graphql layer remains transparent and flexible.

I.e. this way @stubailo's second suggestion (S3 storage etc.) would be easy to implement / customize using already existing packages.

@HriBB
Copy link

HriBB commented Sep 22, 2016

If possible, I would prefer to send everything with a single request.

I am trying to understand, what would need to be done, to support this on the apollo client/server, and also how we can support other graphql implementations.

I'm looking at the networkInterface code, and I think it could be possible by using a middleware. If I understand the code correctly, it goes something like this: HTTPFetchNetworkInterface has a public method fetchFromRemoteEndpoint which receives request and options objects from the middleware chain. It then makes a request using whatwg-fetch. By default is uses Content-Type: application/json, but I think we can override it to multipart/form-data and append uploaded files to the body together with the original request. @stubailo can you confirm this? Then there's HTTPBatchedNetworkInterface and addQueryMerging ... and my head starts to hurt :)

Anyway, if we assume that the client makes a single multipart/form-data request with a mutation, variables and all the files, we can then use something like app.use(apolloKoaUpload()) middleware on the server, process the request, put everything in the right place, and pass it to the apollo server integration (Express, Koa, HAPI). Server can then process it just like any other mutation, except this time, there will be a files parameter available on the resolver method. @helfer @nnance what do you guys think? Maybe I'm missing something, but I think that this could work at least for Express and Koa. Not sure about HAPI. Need to do some research for that.

I can start working on PR(s), but I would love to get some feedback from the core devs first.

@helfer
Copy link

helfer commented Sep 27, 2016

@HriBB I think that's a pretty reasonable approach. On the server side I would put the files on the context initially, and then try creating a special "file" input type.

On the client, how exactly would the user specify a file upload so it gets appended properly in the network layer?

@HriBB
Copy link

HriBB commented Sep 28, 2016

@helfer we could check request variables for the presence of File or FileList similar to this implementation. Or we can add a new option files to the mutate function, but then we would have to add that to the core. Not really sure at this point. @stubailo, what do you think?

@HriBB
Copy link

HriBB commented Oct 16, 2016

I managed to get some work done on this.

CLIENT

Upload middleware will check if any variable is a FileList instance, and will send a FormData if it finds one. I'm using hardcoded key files in the example. Problem is this line, because for the multipart/form-data header to be correctly set, we need to omit Content-Type on the fetch() options. ATM I am using a hacked version of networkInterface.

import { printAST } from 'apollo-client'

function isUpload(request) {
  if (request.variables) {
    for (let key in request.variables) {
      if (request.variables[key] instanceof FileList) {
        return true
      }
    }
  }
  return false
}

export default class UploadMiddleware {
  applyMiddleware(req, next) {
    if (!isUpload(req.request)) return next()

    const body = new FormData()
    const variables = {}

    for (let key in req.request.variables) {
      let v = req.request.variables[key]
      if (v instanceof FileList) {
        Array.from(v).forEach(f => body.append('files', f))
      } else {
        variables[key] = v
      }
    }

    body.append('operationName', req.request.operationName)
    body.append('query', printAST(req.request.query))
    body.append('variables', JSON.stringify(variables))

    req.options.body = body

    next()
  }
}

Use it like any other middleware

import ApolloClient, { createNetworkInterface } from 'apollo-client'
import UploadMiddleware from './UploadMiddleware'

const networkInterface = createNetworkInterface(`http://localhost:4000/graphql`)

networkInterface.use([
  new UploadMiddleware(),
])

Mutation with files, simply pass FileList as a variable.

import React, { Component, PropTypes } from 'react'
import { compose, graphql } from 'react-apollo'
import gql from 'graphql-tag'

class AddImages extends Component {

  static propTypes = {
    gallery: PropTypes.object.isRequired,
  }

  constructor(props) {
    super(props)
    this.files = null
  }

  upload = () => {
    const { gallery, uploadGalleryImages } = this.props
    uploadGalleryImages(gallery.id, this.files)
      .then(({ data }) => {
        console.log('data', data);
      })
      .catch(error => {
        console.log('error', error.message);
      })
  }

  changeFiles = (e) => {
    this.files = e.target.files
  }

  render() {
    return (
      <form>
        <input type={'file'} name={'files'} onChange={this.changeFiles} multiple />
        <button onClick={this.upload}>Upload</button>
      </form>
    )
  }

}

const UPLOAD_GALLERY_IMAGES = gql`
  mutation uploadGalleryImages($id: String!, $files: [UploadedFile!]!) {
    uploadGalleryImages(id: $id, files: $files) {
      id
      slug
      name
    }
  }`

export default compose(
  graphql(UPLOAD_GALLERY_IMAGES, {
    props: ({ ownProps, mutate }) => ({
      uploadGalleryImages: (id, files) => mutate({
        variables: { id, files },
      }),
    }),
  }),
)(AddImages)

SERVER

Upload middleware for apolloKoa(). Will check if request made to /graphql endpoint is multipart, and will process ctx.request.body. In this case I use async-busboy to parse form data.

import asyncBusboy from 'async-busboy'

export default function apolloKoaUpload(options) {

  function isUpload(ctx) {
    return Boolean(
      ctx.path === options.endpointURL &&
      ctx.request.method === 'POST' &&
      ctx.request.is('multipart/*')
    )
  }

  return async function(ctx, next) {
    if (!isUpload(ctx)) return next()
    const { files, fields } = await asyncBusboy(ctx.req)
    const { operationName, query, variables } = fields
    ctx.request.body = {
      operationName,
      query,
      variables: Object.assign(JSON.parse(variables), { files }),
    }
    return next()
  }
}

Use it before apolloKoa()

app.use(apolloKoaUpload({
  endpointURL: '/graphql'
}))

app.use(post('/graphql', apolloKoa({
  schema
})))

app.use(get('/graphiql', graphiqlKoa({
  endpointURL: '/graphql'
})))

Add UploadedFile scalar to schema

scalar UploadedFile

Also add resolver - a simple JSON will do for now ...

const resolveFunctions = {
  UploadedFile: {
    __parseLiteral: parseJSONLiteral,
    __serialize: value => value,
    __parseValue: value => value,
  },
  ...
}

function parseJSONLiteral(ast) {
  console.log('ast', ast);
  switch (ast.kind) {
    case Kind.STRING:
    case Kind.BOOLEAN:
      return ast.value;
    case Kind.INT:
    case Kind.FLOAT:
      return parseFloat(ast.value);
    case Kind.OBJECT: {
      const value = Object.create(null);
      ast.fields.forEach(field => {
        value[field.name.value] = parseJSONLiteral(field.value);
      });

      return value;
    }
    case Kind.LIST:
      return ast.values.map(parseJSONLiteral);
    default:
      return null;
  }
}

So the only thing that's blocking me ATM is the Content-Type header on the fetch() options inside NetworkInterface. @stubailo @helfer what do you think?

@HriBB
Copy link

HriBB commented Oct 18, 2016

Some update

I think it's actually better to use a custom network interface instead of middleware.
I have created UploadNetworkInterface that extends HTTPFetchNetworkInterface.
Before fetching, it checks if the request contains any uploaded files.
If it does, it uses FormData, otherwise JSON body, and sets appropriate headers.

UploadNetworkInterface.js

import { printAST } from 'apollo-client'
import {
  HTTPFetchNetworkInterface,
  addQueryMerging,
  printRequest,
} from 'apollo-client/networkInterface'

export class UploadNetworkInterface extends HTTPFetchNetworkInterface {

  fetchFromRemoteEndpoint(req) {
    const options = this.isUpload(req)
      ? this.getUploadOptions(req)
      : this.getJSONOptions(req)
    return fetch(this._uri, options);
  }

  isUpload({ request }) {
    if (request.variables) {
      for (let key in request.variables) {
        if (request.variables[key] instanceof FileList) {
          return true
        }
      }
    }
    return false
  }

  getJSONOptions({ request, options }) {
    return Object.assign({}, this._opts, {
      body: JSON.stringify(printRequest(request)),
      method: 'POST',
    }, options, {
      headers: Object.assign({}, {
        Accept: '*/*',
        'Content-Type': 'application/json',
      }, options.headers),
    })
  }

  getUploadOptions({ request, options }) {
    const body = new FormData()
    const variables = {}

    for (let key in request.variables) {
      let v = request.variables[key]
      if (v instanceof FileList) {
        Array.from(v).forEach(f => body.append(key, f))
      } else {
        variables[key] = v
      }
    }

    body.append('operationName', request.operationName)
    body.append('query', printAST(request.query))
    body.append('variables', JSON.stringify(variables))

    return Object.assign({}, this._opts, {
      body,
      method: 'POST',
    }, options, {
      headers: Object.assign({}, {
        Accept: '*/*',
      }, options.headers),
    })
  }

}

export function createNetworkInterface(opts) {
  const { uri } = opts
  return addQueryMerging(new UploadNetworkInterface(uri, opts))
}

You can simply use it like this

import { createNetworkInterface } from './UploadNetworkInterface'

const networkInterface = createNetworkInterface({
  uri: `http://localhost:4000/graphql`
})

And a slight improvement of server middleware, to support variable names

import asyncBusboy from 'async-busboy'

export default function apolloKoaUpload(options) {

  function isUpload(ctx) {
    return Boolean(
      ctx.path === options.endpointURL &&
      ctx.request.method === 'POST' &&
      ctx.request.is('multipart/*')
    )
  }

  return async function(ctx, next) {
    if (!isUpload(ctx)) return next()
    const { files, fields } = await asyncBusboy(ctx.req)
    const { operationName, query } = fields
    const variables = JSON.parse(fields.variables)
    files.forEach(file => {
      if (!variables[file.fieldname]) {
        variables[file.fieldname] = []
      }
      variables[file.fieldname].push(file)
    })
    ctx.request.body = {
      operationName,
      query,
      variables,
    }
    return next()
  }
}

I will try to put this into a repo and also create Express example.

Would love to get some feedback on this.

@HriBB
Copy link

HriBB commented Oct 18, 2016

@jangerhofer what do you think?

@zimme
Copy link

zimme commented Oct 18, 2016

@stubailo

Alternative:

(1-3 from above)
There's a server middleware that pulls files off the body, uploads them to a file storage service, and replaces the variables with a reference to that file on S3 or wherever
In your mutation arguments, you get a reference to a file that has been uploaded to S3 already
Other options?

If I was using s3 or some other file service I would not like to upload anything to my graphql server but rather get a token and have my users directly upload to s3/whatever.

@HriBB
Copy link

HriBB commented Oct 18, 2016

@zimme you don't need any of this stuff in your case. But you could write a middleware that uploads files to s3/whatever, replaces variables with tokens and pass them to the mutation.

My proposal involves both apollo client and server.

@zimme
Copy link

zimme commented Oct 18, 2016

Yeah, I guess a regular query to get a valid upload token for s3 and use that to directly upload to s3 would be enough in that case.

@sandervanhooft
Copy link

So, if I have a REST endpoint which handles form input with file upload, what would be the recommended approach with Graphql Server? Should I split the file upload endpoint from the form data endpoint in order to have this supported in Graphql Server?

@HriBB
Copy link

HriBB commented Oct 22, 2016

You can always re-use REST endpoint for file uploads. But then you cannot change client data with mutations when doing file uploads.

@HriBB
Copy link

HriBB commented Oct 22, 2016

And the Express upload middleware. Will put this into a repo when I find some spare time.

export default function apolloExpressUpload(options) {

  function isUpload(req) {
    return Boolean(
      req.baseUrl === options.endpointURL &&
      req.method === 'POST' &&
      req.is('multipart/form-data')
    )
  }

  return function(req, res, next) {
    if (!isUpload(req)) return next()
    const { files, body } = req
    const { operationName, query } = body
    const variables = JSON.parse(body.variables)
    // append files to variables
    files.forEach(file => {
      if (!variables[file.fieldname]) {
        variables[file.fieldname] = []
      }
      variables[file.fieldname].push(file)
    })
    req.body =  {
      operationName,
      query,
      variables,
    }
    return next()
  }

}

Use it like this.

const upload = multer({
  dest: '/uploads',
})

app.use('/graphql',
  upload.array('files'),
  apolloExpressUpload({
    endpointURL: '/graphql'
  }),
  apolloExpress(async (req) => {
    const user = await getAuthenticatedUser(req)
    return {
      schema,
      context: { user }
    }
  }),
)

Not really sure if applying multiple middleware is a good / the best approach, but it works!

@sandervanhooft
Copy link

@HriBB Looks interesting!

Do I understand correctly that I can do file uploads using a GraphQL mutation this way, by assigning the file to a mutation field?

@HriBB
Copy link

HriBB commented Oct 26, 2016

Yes. I have implemented this in two apps now, and it works well so far.

@thebigredgeek
Copy link
Contributor

@HriBB could you implement and publish a reusable higher-order function to decorate the network interface on the client and middleware for the server (express, for instance)?

@HriBB
Copy link

HriBB commented Nov 15, 2016

@thebigredgeek I have created two packages: apollo-upload-network-interface and graphql-server-express-upload middleware. Let me know if it works for you. Would love to get some feedback on this, especially from core devs.

I only tested locally using npm link method. Too tired now, gotta get some sleep.

@thebigredgeek
Copy link
Contributor

@HriBB Awesome! I submitted an issue (more of a suggestion, really) here. Take a look when you have a chance!

@sandervanhooft
Copy link

@stubailo, @helfer Shouldn't the file upload features be considered part of the 1.0 Apollo Client release? I think the method proposed by @HriBB is a great starting point.

@helfer
Copy link

helfer commented Nov 30, 2016

@sandervanhooft can you open a new issue to have a discussion about what should and shouldn't be in the 1.0 milestone? That way it will be easier for other people to find. If there's enough demand for features that aren't currently part of it, we'll consider adding them.

@sandervanhooft
Copy link

@helfer Thanks for the suggestion!. Done.

@guilhermedecampo
Copy link

@helfer @sandervanhooft @HriBB @stubailo

You guys have a suggestion to make it easy/seamless using react-native?

Since we just create an object for the file-upload the check logic does not fit because it relies on FileList / File instanceof as you can see here https://github.com/HriBB/apollo-upload-network-interface/blob/master/src/UploadNetworkInterface.js#L21 and here https://github.com/HriBB/apollo-upload-network-interface/blob/master/src/UploadNetworkInterface.js#L47.

Thanks!

@sandervanhooft
Copy link

sandervanhooft commented Nov 30, 2016 via email

@wmertens
Copy link

wmertens commented Feb 9, 2017

@felixk42 indeed, you have to use XHR until fetch supports progress.

I just found this, great! I considered doing the same but then reasoned that it would require changing the networkinterface and it wouldn't work for all transports.

Instead I implemented upload via REST, which returns an id that will be used by a graphql mutation to store the file in a relevant object. That is pretty straightforward, but having a standard way to do it in Apollo allows choosing how/where you upload, separately from your app logic.

So if Apollo supports this as a separate concept from the NetworkInterface via a side-channel with the server, you could plug in form upload, S3 pre-signed encrypted upload etc, you could even send a stream via websocket. The mutation on the server would be executed only once the file is uploaded.

Of course, uploading a file is non-trivial, so the mutation has to handle rollback in case the upload succeeds but the mutation fails.

Furthermore, it could be useful to let a mutation weigh in on whether an upload is allowed. So the upload side-channel should also allow inspecting the mutation with variables before the upload starts.

So, I'm seeing these required parts:

  • On the client: pluggable uploader
  • On the server: pluggable upload receiver, with validation callback
  • In GraphQL: e.g. ApolloUpload scalar, that takes a File-like object on the client and provides a plugin-dependent token on the server (could be a filesystem path, S3 id etc)

This is how I think it would work:

  • Client calls mutation with File-like object
  • Side-channel requests validation as well as any extra information like S3 pre-signed URL
  • Upon validation, file upload(s) start, (with e.g. progress callback provided in mutation call)
  • Once upload(s) complete, graphql mutation is called using the regular process, the ApolloUpload fields containing the token(s)

IMHO this would provide lots of benefit without much change to the existing graphql infrastructure; it would even work with a non-apollo graphql server.

@felixk42
Copy link

@wmertens Yep for now I am modifying the networkInterface to make it work.
And yeah I think a side-channel and passing a reference to the file is the way to go.

@sandervanhooft
Copy link

@wmertens: using the sideloader would mean you need to make 2 calls in total as I understand it. If you have for example an unstable mobile connection, rolling back would get really complex.

I'd prefer a single atomic graphql call.

@wmertens
Copy link

wmertens commented Feb 19, 2017 via email

@sandervanhooft
Copy link

sandervanhooft commented Feb 19, 2017 via email

@jaydenseric
Copy link
Contributor

Just published apollo-upload-server and apollo-upload-client. Hopefully this will make things a lot easier in the meantime.

No need to setup multer or deal with custom scalars. It allows you to use File objects, FileList objects, or File arrays anywhere within mutation or query variables. You can name variables whatever, and have multiple sets of files in one mutation. You can decide if you want the files to be in a list or singular. The files upload to a configurable temp directory; the file paths and metadata will be available under the variable name in the resolver.

@wmertens
Copy link

@jaydenseric great! Could you expand on how your solution differs from https://github.com/HriBB/apollo-upload-network-interface ?

@wmertens
Copy link

@jaydenseric Oh, I see now that you already did that when you said "no adding multer, no custom scalars" :)

@jaydenseric
Copy link
Contributor

@wmertens Probably the next most obvious difference is that you can upload single files without having to always use a list. You can also input FileList objects or arrays of files just the same. In theory you can also use more complicated input structures containing files, as files are looked for recursively.

@sandervanhooft
Copy link

sandervanhooft commented Feb 28, 2017 via email

@jaydenseric
Copy link
Contributor

@sandervanhooft Not yet, I intend to add an additional createBatchingNetworkInterface export once I start optimizing the app I'm working on. An earlier pull request would be welcomed 🙂

@wmertens
Copy link

wmertens commented Feb 28, 2017 via email

@jaydenseric
Copy link
Contributor

@sandervanhooft @wmertens apollo-upload-server and apollo-upload-client now support query batching 🎉

import ApolloClient from 'apollo-client'
import {createBatchNetworkInterface} from 'apollo-upload-client'

const client = new ApolloClient({
  networkInterface: createBatchNetworkInterface({
    uri: '/graphql',
    batchInterval: 10
  })
})

apollo-upload-server will automatically handle regular or batch requests.

@wmertens
Copy link

wmertens commented Mar 26, 2017 via email

@lednhatkhanh
Copy link

@jaydenseric Your project is amazing, using for my new project, hope you will keep developing it!

@mrgzi
Copy link

mrgzi commented Jun 4, 2017

When will the Apollographql team support file uploads?

@jamesone
Copy link

@jaydenseric Awesome package! Just got it setup and works so smoothly :)

@giautm
Copy link

giautm commented Jun 20, 2017

@jaydenseric: I have to copied and modified your code to make it work with react-native. :p.
I created a class named FileInput

/**
 * @providesModule FileInput
 * @flow
 */
// On Android we need a polyfill for Symbol
import Symbol from 'es6-symbol/polyfill';

const IsFile = Symbol('is file');
const IsFileList = Symbol('is file list');

export default class FileInput {
  constructor ({ name, type, uri } = {}) {
    this[IsFile] = true
    this.name = name
    this.type = type
    this.uri = uri
  }

  static fromArray = (array, defaultType = 'image/png') => Array.isArray(array) ?
    array.map(({ name, type, uri }, index) => new FileInput({
      name: name || `file_${index}`,
      type: type || defaultType,
      uri,
    })) : null;
}

if (typeof window !== 'undefined') {
  if (typeof window.File !== 'undefined') {
    window.File[IsFile] = true;
  }
  if (typeof window.FileList !== 'undefined') {
    window.File[IsFileList] = true;
  }
}

export const isFileInput = (node) => node && node[IsFile];
export const isFileInputList = (node) => node && (
  (Array.isArray(node) && node.every(isFileInput)) || node[IsFileList]);

And edited in function extractRequestFiles:

import RecursiveIterator from 'recursive-iterator';
import objectPath from 'object-path';

import {
  isFileInput,
  isFileInputList,
} from './FileInput';

/**
 * Extracts files from an Apollo Client Request, remembering positions in variables.
 * @see {@link http://dev.apollodata.com/core/apollo-client-api.html#Request}
 * @param {Object} request - Apollo GraphQL request to be sent to the server.
 * @param {Object} request.variables - GraphQL variables map.
 * @param {string} request.operationName - Name of the GraphQL query or mutation.
 * @returns {Object} - Request with files extracted to a list with their original object paths.
 */
export function extractRequestFiles(request) {
  const files = [];
  let variablesPath;

  // Recursively search GraphQL input variables for FileList or File objects
  for (let { node, path } of new RecursiveIterator(request.variables)) {
    const isFile = isFileInput(node); /// <----- Edit here
    const isFileList = isFileInputList(node); /// <----- And here.

    if (isFileList || isFile) {
      // Only populate when necessary
      if (!variablesPath) {
        variablesPath = objectPath(request.variables);
      }

      const pathString = path.join('.');

      if (isFileList) {
        // Convert to FileList to File array. This is
        // necessary so items can be manipulated correctly
        // by object-path. Either format may be used when
        // populating GraphQL variables on the client.
        variablesPath.set(pathString, Array.from(node));
      } else if (isFile) {
        // Move the File object to a multipart form field
        // with the field name holding the original path
        // to the file in the GraphQL input variables.
        files.push({
          file: node,
          variablesPath: `variables.${pathString}`,
        });
        variablesPath.del(pathString);
      }
    }
  }

  return {
    operation: request,
    files,
  };
}

Also on each network-interface, i removed window check.

And every things work as expect. Happing coding. :p

Ps: Copied from the old version of apollo-upload-client

@stubailo
Copy link
Contributor

stubailo commented Jul 7, 2017

Hello everyone! It's cool that there is some community code floating around and even a package - what's in between this and including it as official thing? Almost all of the main Apollo Client features were built initially by the community. If you have a great idea for file uploads, let's make it happen! Anybody up for it?

@giautm
Copy link

giautm commented Jul 7, 2017

@stubailo apollo-upload-client 5.0.0 supported upload for react and react-native.

@jaydenseric
Copy link
Contributor

jaydenseric commented Jul 7, 2017

@stubailo I would be keen to work with you. apollo-upload-client and apollo-upload-server could totally be integrated into the official projects. It would be a good opportunity to make a few improvements while we're at it.

Basically, the network interface crawls operation variables to find files. If there are any, it prepares a multipart request. There is a convention for how the files get split out into their own multipart fields so that the server can identify that the variables need to be reassembled, and how.

In theory, you could also add an alternative Base64 encoding strategy to support vanilla POST and GET requests. It's not something that excites me because it's less efficient and I don't use GET.

Something that Apollo does not have (?), is an API for tracking loading progress. That would be really nice to tap into for heavier requests containing files. I guess that would mean abandoning fetch though.

It's been on my mind to investigate providing file streams in resolver arguments instead of file path strings. This would allow people to stream the file straight into MongoDB GridFS, etc. if they so choose or store it directly where they would like on the server.

@stubailo
Copy link
Contributor

stubailo commented Jul 7, 2017

Wow, just looked more at those packages and they look pretty straightforward to use! It looks like the client side is more finalized than the server, where it's not exactly clear where the files should go. So maybe we can start there?

@jaydenseric
Copy link
Contributor

jaydenseric commented Jul 7, 2017

@stubailo There seems to be a lot of change coming to transport in Apollo with the new fetcher, etc. Will we implement with the current, or future interfaces? jaydenseric/apollo-upload-client#21 (comment)

@stubailo
Copy link
Contributor

stubailo commented Jul 7, 2017

It feels like the new apollo-fetch is definitely a great place to add this functionality. Let's discuss there? apollographql/apollo-fetch#6

@stubailo
Copy link
Contributor

stubailo commented Jul 7, 2017

Something that Apollo does not have (?), is an API for tracking loading progress. That would be really nice to tap into for heavier requests containing files. I guess that would mean abandoning fetch though.

This is something that wouldn't be part of apollo-fetch but might fit nicely into a websocket transport and the new apollo-link stuff we've been thinking about - basically converting the network layer from being promise-based to observable-based. Let's get the basic thing done and then we can see where we can go from there! Really excited to work with you.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests