graphql
graphql copied to clipboard
How to upload multiple files?
I try to do it in following way:
clt := mgql.NewClient("http://0.0.0.0:7071/graphql", mgql.UseMultipartForm())
query := `
mutation ($files: [Upload!]!) {
addLpFiles( input: { files: $files })
{success}}`
req := mgql.NewRequest(query)
for i, s := range []string{"aaa", "bbb"} {
r := strings.NewReader(s)
req.File("files."+strconv.Itoa(i), s, r)
//req.File(strconv.Itoa(i), s, r)
}
err := clt.Run(ctx, req, nil)
if err != nil {
return err
}
... and get error - graphql: first part must be operations.
PS: without .UseMultipartForm() usual read queries go well
The spec expects that there should be operations, map and file fields - e.g.:
curl localhost:3001/graphql \
-F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \
-F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \
-F [email protected] \
-F [email protected]
Here is amended implementation of runWithPostFields
func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp interface{}) error {
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
oprField, err := operationsField(req)
if err != nil {
return errors.Wrap(err, "encode operations")
}
oprWriter, err := writer.CreateFormField("operations")
if err != nil {
return errors.Wrap(err, "encode operations")
}
_, err = oprWriter.Write(oprField)
if err != nil {
return errors.Wrap(err, "encode operations")
}
mpField, err := mapField(req, err)
if err != nil {
return errors.Wrap(err, "encode map")
}
mapWriter, err := writer.CreateFormField("map")
if err != nil {
return errors.Wrap(err, "encode map")
}
_, err = mapWriter.Write(mpField)
if err != nil {
return errors.Wrap(err, "encode map")
}
// Add files fields
for i := range req.files {
part, err := writer.CreateFormFile(req.files[i].Field, req.files[i].Name)
if err != nil {
return errors.Wrap(err, "create form file")
}
if _, err := io.Copy(part, req.files[i].R); err != nil {
return errors.Wrap(err, "preparing file")
}
}
if err := writer.Close(); err != nil {
return errors.Wrap(err, "close writer")
}
//c.logf(">> variables: %s", variablesBuf.String())
c.logf(">> files: %d", len(req.files))
c.logf(">> query: %s", req.q)
gr := &graphResponse{
Data: resp,
}
r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody)
if err != nil {
return err
}
r.Close = c.closeReq
r.Header.Set("Content-Type", writer.FormDataContentType())
r.Header.Set("Accept", "application/json; charset=utf-8")
for key, values := range req.Header {
for _, value := range values {
r.Header.Add(key, value)
}
}
c.logf(">> headers: %v", r.Header)
r = r.WithContext(ctx)
res, err := c.httpClient.Do(r)
if err != nil {
return err
}
defer res.Body.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, res.Body); err != nil {
return errors.Wrap(err, "reading body")
}
c.logf("<< %s", buf.String())
if err := json.NewDecoder(&buf).Decode(&gr); err != nil {
if res.StatusCode != http.StatusOK {
return fmt.Errorf("graphql: server returned a non-200 status code: %v", res.StatusCode)
}
return errors.Wrap(err, "decoding response")
}
if len(gr.Errors) > 0 {
// return first error
return gr.Errors[0]
}
return nil
}
// mapField build map field of graphQL multipart query - returns:
//
// { "0": ["variables.files.0"], "1": ["variables.files.1"] }
func mapField(req *Request, err error) ([]byte, error) {
mapField := map[int][]string{}
for i := 0; i < len(req.files); i++ {
mapField[i] = []string{fmt.Sprintf("variables.files.%d", i)}
}
bt, err := json.Marshal(mapField)
if err != nil {
return nil, err
}
return bt, err
}
// operationsField build operations field of graphQL multipart query returns:
//
// { "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }",
//
// "variables": { "file": null } }'
func operationsField(req *Request) ([]byte, error) {
opm := map[string]interface{}{}
opm["query"] = req.q
flsVars := make([]string, len(req.files))
for i, _ := range flsVars {
flsVars[i] = "null"
}
opm["variables"] = map[string][]string{"files": flsVars}
bt, err := json.Marshal(opm)
if err != nil {
return nil, err
}
return bt, err
}
/ File sets a file to upload.
// Files are only supported with a Client that was created with
// the UseMultipartForm option.
//
// In accordance with https://github.com/jaydenseric/graphql-multipart-request-spec
// files should be added in the following form:
//
// "0", "fileName0.txt, fileReader0"
//
// "1", "fileName1.txt, fileReader1
//
// "N", "fileNameN.txt, fileReaderN
func (req *Request) File(fieldname, filename string, r io.Reader) {
req.files = append(req.files, File{
Field: fieldname, Name: filename, R: r,
})
}