apollo-upload-network-interface icon indicating copy to clipboard operation
apollo-upload-network-interface copied to clipboard

Only works with top-level variables

Open thebigredgeek opened this issue 8 years ago • 6 comments

We should probably support FileLists nested in variables. It's common to pass an input type rather than scalars for mutation and query params. It seems like a simple bit of recursion could make this work.

thebigredgeek avatar Dec 07 '16 20:12 thebigredgeek

https://github.com/HriBB/graphql-server-express-upload/issues/7

thebigredgeek avatar Dec 07 '16 20:12 thebigredgeek

Maybe we could introduce a top-level variable isUpload = true to indicate an upload-request, instead of magically looking for a FileList instance.

isUpload({ request }) {
  return request.variables && request.variables.isUpload;
}

Edit: I now realize that we need to deeply parse the variables anyway, because we have to append the FileList to the FormData later on (in getUploadOptions).

jessedvrs avatar Feb 15 '17 09:02 jessedvrs

Fully working example fixing a lot of issues (a mix of your script and the script from here):

import { printAST } from 'apollo-client';
import { HTTPFetchNetworkInterface } from 'apollo-client/transport/networkInterface';
import RecursiveIterator from 'recursive-iterator';
import objectPath from 'object-path';

export default function createNetworkInterface(opts) {
    const { uri } = opts;
    return new UploadHTTPFetchNetworkInterface(uri, opts);
};

class UploadHTTPFetchNetworkInterface extends HTTPFetchNetworkInterface {
    constructor(...args) {
        super(...args);

        const normalQuery = this.fetchFromRemoteEndpoint.bind(this);

        this.fetchFromRemoteEndpoint = ({request, options}) => {
            const formData = new FormData();

            // search for File objects on the request and set it as formData
            let hasFile = false;
            for (let { node, path } of new RecursiveIterator(request.variables)) {
                if (node instanceof File) {
                    hasFile = true;
                    const id = Math.random().toString(36);
                    formData.append(id, node);
                    objectPath.set(request.variables, path.join('.'), id);
                }
            }

            if (hasFile) {
                return this.uploadQuery({request, options}, formData);
            } else {
                return normalQuery({request, options});
            }
        };
    }

    uploadQuery({request, options}, formData) {
        formData.append('operationName', request.operationName);
        formData.append('query', printAST(request.query));
        formData.append('variables', JSON.stringify(request.variables || {}));

        return fetch(this._opts.uri, {
            ...options,
            body: formData,
            method: 'POST',
            headers: {
                Accept: '*/*',
                ...options.headers
            }
        });
    }
};


jessedvrs avatar Feb 15 '17 14:02 jessedvrs

@jessedvrs your comment was really helpful, this feature is supported in apollo-upload-server. I referenced you in the readme 🙂

See here in the src. I created a generic processRequest function that returns a promise, to make it easy to write a library of middlewares for Express, Koa, etc. It's exported, so others can use it to make their own exotic middleware.

jaydenseric avatar Mar 19 '17 14:03 jaydenseric

@jaydenseric happy to help!

jessedvrs avatar Mar 23 '17 08:03 jessedvrs

I've been thinking about this a lot. I think the simplest way to implement this is to recursively traverse the variables object and look for file types such as files, file lists, etc.

If we find files, we should push them into an array leave a token in place of the file object that can be programmatically parsed to determine where in the array the file was placed. If a file list is an encountered, we loop across it and push each file into the array, leaving an array of tokens rather than a single token. Then, we convert to form data and preserve the order of the files. On the server, we can then re-constitute the exact positions of the files using the tokens before we pass the variables into the resolvers.

The trick here is that we only want to worry about this if we are using HTTP. For users using WebSockets as their primary transport, a different solution would need to exist. For this reason, I think it's best for this to live in the NetworkInterface, but I think everyone has already come to that conclusion anyway.

thebigredgeek avatar Mar 23 '17 17:03 thebigredgeek