bytewitchcraft / apollo-absinthe-upload-link

A network interface for Apollo that enables file-uploading to Absinthe back ends.
61 stars 37 forks source link

Documentation/Examples on how to perform an upload #15

Closed coryodaniel closed 6 years ago

coryodaniel commented 6 years ago

I currently have an absinthe graphql api that will accept uploads (from curl). I'm now trying to upload from react native and can't seem to figure out how to do it.

Using the following curl command, I can successfully upload files:

curl -X POST \
 -F query="mutation{uploadPhoto(productId: \"d50e25b0-387a-44ad-98b0-cd0265cefda0\", file: \"file\"){id,url(version: "ORIGINAL")}}" \
 -F file=@test/fixtures/pants.jpg
 localhost:8088/api

Output from elixir - which looked strange to me because the file is a string literal "file" ... but it works

[info] POST /api
[debug] ABSINTHE schema=SpectraWeb.Schema variables=%{}
---
mutation{uploadPhoto(productId: "d50e25b0-387a-44ad-98b0-cd0265cefda0", file: "file"){id,url(version: ORIGINAL)}}
---
[info] Sent 200 in 5137ms

Below is the react component that gets an error back:

import React from "react"
import gql from "graphql-tag"
import { View, Button, TextInput, Image } from "react-native"
import { Constants, Permissions, ImagePicker } from "expo"
import { graphql } from "react-apollo"
import { ReactNativeFile } from "apollo-absinthe-upload-link"

const defaultState = {
  values: {
    photoUrl: ""
  },
  errors: {},
  isSubmitting: false
}

class PhotoUploadScreen extends React.Component {
  state = defaultState

  onChangeValue = (key, value) => {
    this.setState(state => ({
      values: {
        ...state.values,
        [key]: value
      }
    }))
  }

  submit = async () => {
    if (this.state.isSubmitting) { return }
    this.setState({ isSubmitting: true })

    let response
    const { photoUrl } = this.state.values
    const file = new ReactNativeFile({
      uri: photoUrl,
      type: "image/jpeg",
      name: "new-photo"
    })

    try {
      response = await this.props.mutate({
        variables: {
          file
        }
      })
    } catch (err) {
      console.log("err happened", err)
    }
    console.log(response)
    this.setState({ isSubmitting: false })
  }

  pickFromGallery = async () => {
    const permissions = Permissions.CAMERA_ROLL
    const { status } = await Permissions.askAsync(permissions)

    console.log(permissions, status)
    if (status === "granted") {
      let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: "Images" }).catch(
        error => console.log(permissions, { error })
      )

      if (!result.cancelled) {
        console.log(permissions, "SUCCESS", result)
        this.onChangeValue("photoUrl", result.uri)
      }
    }
  }

  render() {
    const { values: { photoUrl } } = this.state
    return (
      <View>
        <View>
          <Button title="Pick from Gallery" onPress={this.pickFromGallery} />
          {photoUrl ? (<Image source={{ uri: photoUrl }} />) : null}
          <Button title="Upload" onPress={this.submit} />
        </View>
      </View>
    )
  }
}

const uploadPhotoMutation = gql`
  mutation uploadPhoto($file: Upload!) {
    uploadPhoto(productId: "d50e25b0-387a-44ad-98b0-cd0265cefda0", file: $file) {
      id
      url(version: "ORIGINAL")
    }
  }
`

export default graphql(uploadPhotoMutation)(PhotoUploadScreen)

It looks as though the ReactNativeFile is getting serialized to "file" in the variables map, but the file does actually get submitted (I can see it in wireshark)

[debug] ABSINTHE schema=SpectraWeb.Schema variables=%{"file" => "file"}
---
mutation uploadPhoto($file: Upload!) {
  uploadPhoto(productId: "d50e25b0-387a-44ad-98b0-cd0265cefda0", file: $file) {
    id
    url(version: "ORIGINAL")
    __typename
  }
}
---
[info] Sent 400 in 2009ms

Thanks in advance for any guidance! :D

coryodaniel commented 6 years ago

Looking at the http requests coming into absinthe/plug ...

Here is the one that works (from curl):

%Plug.Conn{
  body_params: %{
    "file" => %Plug.Upload{
      content_type: "image/jpeg",
      filename: "pants.jpg",
      path: "/var/folders/x3/yf3cmyj54mn_59qh3xcyl3wc0000gn/T//plug-1541/multipart-1541180969-847941438387443-1"
    },
    "query" => "mutation{uploadPhoto(productId: \"d50e25b0-387a-44ad-98b0-cd0265cefda0\", file: \"file\"){id,url(version: ORIGINAL)}}"
  },
  params: %{
    "file" => %Plug.Upload{
      content_type: "image/jpeg",
      filename: "ruins.jpg",
      path: "/var/folders/x3/yf3cmyj54mn_59qh3xcyl3wc0000gn/T//plug-1541/multipart-1541180969-847941438387443-1"
    },
    "query" => "mutation{uploadPhoto(productId: \"d50e25b0-387a-44ad-98b0-cd0265cefda0\", file: \"file\"){id,url(version: ORIGINAL)}}"
  },

  req_headers: [
    {"host", "localhost:8088"},
    {"user-agent", "curl/7.54.0"},
    {"accept", "*/*"},
    {"content-length", "795873"},
    {"expect", "100-continue"},
    {"content-type",
     "multipart/form-data; boundary=------------------------949cb840cd576ddd"}
  ]
}

Here is the request from React Native that fails:

%Plug.Conn{
  body_params: %{
    "query" => "mutation uploadPhoto($file: Upload!) {\n  uploadPhoto(productId: \"d50e25b0-387a-44ad-98b0-cd0265cefda0\", file: $file) {\n    id\n    url(version: \"ORIGINAL\")\n    __typename\n  }\n}\n",
    "variables" => "{\"file\":\"file\"}"
  },
  params: %{
    "query" => "mutation uploadPhoto($file: Upload!) {\n  uploadPhoto(productId: \"d50e25b0-387a-44ad-98b0-cd0265cefda0\", file: $file) {\n    id\n    url(version: \"ORIGINAL\")\n    __typename\n  }\n}\n",
    "variables" => "{\"file\":\"file\"}"
  },
  req_headers: [
    {"host", "localhost:8088"},
    {"content-type",
     "multipart/form-data; boundary=ECv.0QQlvvCy0Sy9Gi7KfTrrO1N0BkXj_AQL5k6kW20cLl7.5OvzjQloA0_pVumS4qDEza"},
    {"user-agent", "Expo/2.5.9.1014764 CFNetwork/897.15 Darwin/17.4.0"},
    {"accept", "*/*"},
    {"accept-language", "en-us"},
    {"content-length", "3834787"},
    {"accept-encoding", "br, gzip, deflate"},
  ]
}
coryodaniel commented 6 years ago

Turns out both ways work :D Had a syntax error w/ Enum in Apollo :(