graphql icon indicating copy to clipboard operation
graphql copied to clipboard

How to upload multiple files?

Open palandlom opened this issue 1 year ago • 1 comments

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

palandlom avatar Nov 20 '24 18:11 palandlom

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

palandlom avatar Nov 23 '24 13:11 palandlom