react-relay-network-modern
react-relay-network-modern copied to clipboard
Add File Upload support by spec
Jayden Seric wrote an amazing spec for file upload: https://github.com/jaydenseric/graphql-multipart-request-spec
It has an implementation for most servers express, koa, also used in Apollo and Prisma. Will be nice if somebody takes care about writing a new fileUpload middleware for this package.
+1 for this feature. It will enhances the interoperability of this library to the further stage.
I made the middleware that mutates the req to match the spec, if there's any uploadable exists in req.
This middleware is working fine for my current envirionment. I've confirmed that it works well with Reference middleware implementation for the express-server But not sure if it works for any other enviornments. I'm just leaving the middleware code here as reference for someone who looking for the actual implementation of RRNM middleware.
const multipartRequestMiddleware = next => async (req) => {
const {
id, operation: { text: query }, variables, uploadables,
} = req;
if (uploadables) {
const graphQLMultipart = new FormData();
const fileMap = [];
const writeMapFromFileAndMarkVariable = (searchable, parents) => {
Object.keys(searchable).forEach((key) => {
const currentValue = searchable[key];
if (typeof currentValue === 'object' && (currentValue.constructor === Object || currentValue.constructor === Array)) {
// Recursive
writeMapFromFileAndMarkVariable(currentValue, [...parents, key]);
} else {
// Consider the non-plain objects inside uplodables as File data.
fileMap.push({
operationPath: ['variables', ...parents, key].join('.'),
file: currentValue,
});
// Synchronize variable with uploadables.
let currentDepthVariable = variables;
parents.forEach((parent) => {
if (!currentDepthVariable[parent]) {
currentDepthVariable[parent] = {};
}
currentDepthVariable = currentDepthVariable[parent];
});
currentDepthVariable[key] = null; // Spec: Value of file key should be null.
}
});
};
writeMapFromFileAndMarkVariable(uploadables, []);
const operations = {
id,
query,
variables,
};
graphQLMultipart.append('operations', JSON.stringify(operations));
graphQLMultipart.append('map', JSON.stringify(fileMap.reduce((reducedMap, value, index) => ({ ...reducedMap, [index]: [value.operationPath] }), {})));
fileMap.forEach((mapValue, index) => {
graphQLMultipart.append(index, mapValue.file);
});
req.fetchOpts.body = graphQLMultipart;
}
const res = await next(req);
return res;
};
The middleware deep-merges and mutates value of file key inside variable to null, matching the specification. Here's example usage.
const variables = {
input: {
name: 'John doe',
contact: '000-0000-0000',
},
};
const uploadables = {
input: {
resume: files[0],
},
fieldNotExist: {
exampleFile: files[1],
},
};
/* variables in actual request will looks like:
variables: {
input: {
name: 'John doe',
contact: '000-0000-0000',
resume: null,
},
fieldNotExist: {
exmapleFile: null
}
}
*/
commitMutation(
environment,
{
applyMutation,
variables,
uploadables,
},
);
I'm also not sure this will works if there's array of files, as I did consider that case but not yet tested. Will appreciate any feedbacks :)
This has served me well:
// uploadMiddleware.js
// @flow
import RelayRequestBatch from 'react-relay-network-modern/lib/RelayRequestBatch';
import { type Middleware } from 'react-relay-network-modern/lib/definition';
import { extractFiles } from 'extract-files';
export default function uploadMiddleware(): Middleware {
return (next) => async (req) => {
if (req instanceof RelayRequestBatch) {
throw new Error('RelayRequestBatch is not supported');
}
const operations = {
query: req.operation.text,
variables: req.variables,
};
const { clone: extractedOperations, files } = extractFiles(operations);
if (files.size) {
const formData = new FormData();
formData.append('operations', JSON.stringify(extractedOperations));
const pathMap = {};
let i = 0;
files.forEach((paths) => {
pathMap[++i] = paths;
});
formData.append('map', JSON.stringify(pathMap));
i = 0;
files.forEach((paths, file) => {
formData.append(++i, file, file.name);
});
req.fetchOpts.method = 'POST';
req.fetchOpts.body = formData;
}
const res = await next(req);
return res;
};
}
@AlicanC would you like open a pull request with this new middleare?
what about headers ?
i have an issue bcs my php server receives empty body:
------WebKitFormBoundarylH56I1IGAU5mDtsx
Content-Disposition: form-data; name="operations"
{"variables":{"merchantId":"7f563392ca6645deb2aaac28606d1b8d","data":{"virtual":true,"description":"Merchant description","categoryId":"94C02F5CC1C44D8186DFF63A17D18113","share":3,"active":true,"name":"Merchant name"}},"query":"mutation updateMerchantMutation( $merchantId: ID! $data: MerchantInput!) { merchant: updateMerchant(id: $merchantId, data: $data) { id name description virtual category { id name } shops { id address { location { lng lat } country street number city zip } } image {id} couponTemplates { id count value validUntil } }}","id":"updateMerchantMutation"}
------WebKitFormBoundarylH56I1IGAU5mDtsx
Content-Disposition: form-data; name="map"
{ "0": [ "variables.data.image" ] }
------WebKitFormBoundarylH56I1IGAU5mDtsx
Content-Disposition: form-data; name="0"; filename="image.png"
Content-Type: image/png
PNG
IHDRé IDATx^ìtUÙþWãww
î.»»»»»wiÆàî@Ü«ÿµ÷
î~3ofÞêùÏLÎ}ë¤CîÎ=uöþNÕWU_©,Å
Yùú2Ø¿ý§òU±bÅË*«GF+ÑrÓû»
Àü-s*oV, XàÏPFÙü0¨LwNZ±Ú¢$kÖ,óÃL,¬X ýY@eLzó;þ._¹bÅ~¤ùÖUÖV,Î- L:ßÊå+øPæGZWY[±@:·0é|(¯XàGZ@i]emÅéÜ
but server says:
"Request is expected to provide parsed body for \"multipart/form-data\" requests but got empty array
Postman example working perfectly so,its not problem on the server side i hope
for future generations, u can save few hours with delete req.fetchOpts.headers['Content-Type'];
multipart/form-data; boundary=----WebKitFormBoundaryAt1JtNjc2JMupywN
that boundary part is crucial and u cannot set it properly so just delete this header
so i highly recoment to put it right after
req.fetchOpts.body = graphQLMultipart;
and make sure your middleware is the last one or nothing behind could change this header
@nodkz @AlicanC If this middleware was somehow tested, then probably later it was broken. Might be that I am wrong, could you please confirm if it working for you? do you have an example of the mutation (below mine)?
As nowadays, if (files.size) { returns always 0.
https://github.com/relay-tools/react-relay-network-modern/blob/bf0b16f7a008bdaf72e97a92eeddf9ee4d2d7a38/src/middlewares/upload.js#L19
The version fro @jaydenseric looks different, mainly we need to pass req.operation into extractFiles to get access to uploadables.
https://github.com/jaydenseric/graphql-react/blob/b86c916ad535cef9c5594515be556663779d85a1/fetchOptionsGraphQL.mjs#L31
// example
commitMutation(environment, {
mutation: graphql`
mutation trial1Mutation($images: [Upload!]!) {
uploadImages(images: $images)
}
`,
variables: {
images: [],
},
uploadables: {
'variables.images': acceptedFiles,
},
onCompleted: ({uploadImages}) => {
console.log('complete ===>', uploadImages);
},
onError: (e) => {
console.log('error ===>', e);
},
});
I cannot confirm. Right now I don't use Relay in my projects. Moreover I don't use file uploading via graphql. And I recommend to use S3 signed-urls - we just give user urls for file uploading via graphql and users upload directly their files to object storage.
Anyway if somebody figure out this problem and open Pull Request then I'll gladly accept it.
I cannot confirm. Right now I don't use Relay in my projects. Moreover I don't use file uploading via graphql. And I recommend to use S3 signed-urls - we just give user urls for file uploading via graphql and users upload directly their files to object storage.
Anyway if somebody figure out this problem and open Pull Request then I'll gladly accept it.
@nodkz I am re-checking. I used uploadables config of the mutation as shown in the docs, but I see that this implementation uses variables only and also works.
It seems simpler, and even makes more sense. I will try to recheck with some of the usual Relay contributors. Which is the right approach? Is one of the legacy or better than the other?
CC @kassens @josephsavona @sibelius
Here my code refactored:
commitMutation(getEnvironment(), {
mutation: graphql`
mutation trial1Mutation($images: [Upload!]!) {
uploadImages(images: $images)
}
`,
variables: {
images: acceptedFiles,
},
onCompleted: ({uploadImages}) => {
console.log('complete ===>', uploadImages);
},
onError: (e) => {
console.log('error ===>', e);
},
});