Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only works with top-level variables #5

Open
thebigredgeek opened this issue Dec 7, 2016 · 6 comments · May be fixed by #15
Open

Only works with top-level variables #5

thebigredgeek opened this issue Dec 7, 2016 · 6 comments · May be fixed by #15

Comments

@thebigredgeek
Copy link
Collaborator

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
Copy link
Collaborator Author

@jessedvrs
Copy link

jessedvrs commented Feb 15, 2017

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
Copy link

jessedvrs commented Feb 15, 2017

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
            }
        });
    }
};

@jaydenseric
Copy link

@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.

@jessedvrs
Copy link

@jaydenseric happy to help!

@jessedvrs jessedvrs linked a pull request Mar 23, 2017 that will close this issue
@thebigredgeek
Copy link
Collaborator Author

thebigredgeek commented Mar 23, 2017

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.

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

Successfully merging a pull request may close this issue.

3 participants