Code Monkey home page Code Monkey logo

apollo-absinthe-upload-link's Introduction

Build Status

Apollo-Absinthe-Upload-Link

A network interface for Apollo that enables file-uploading to Absinthe back ends.

Usage

Install via yarn or npm and then use createLink from the package in the construction of your ApolloClient-instance.

import ApolloClient from "apollo-client";
import { createLink } from "apollo-absinthe-upload-link";

const client = new ApolloClient({
    link: createLink({
        uri: "/graphql"
    })
});

Custom headers

Custom headers can be passed through options of the link.

import ApolloClient from "apollo-client";
import { createLink } from "apollo-absinthe-upload-link";

const headers = { authorization: 1234 } 
const client = new ApolloClient({
    link: createLink({
        uri: "/graphql"
    }),
    headers,
});

Custom fetch

You can use the fetch option when creating an apollo-absinthe-upload-link to do a lot of custom networking. This is useful if you want to modify the request based on the calculated headers or calculate the uri based on the operation.

import ApolloClient from "apollo-client";
import { createLink } from "apollo-absinthe-upload-link";

const customFetch = (uri, options) => {
  const { header } = Hawk.client.header(
    "http://example.com:8000/resource/1?b=1&a=2",
    "POST",
    { credentials: credentials, ext: "some-app-data" }
  );
  options.headers.Authorization = header;
  return fetch(uri, options);
};

const headers = { authorization: 1234 } 
const client = new ApolloClient({
    link: createLink({
        uri: "/graphql"
    }),
    headers,
    fetch: customFetch
});

Usage with React Native

Substitute File with ReactNativeFile:

import { ReactNativeFile } from 'apollo-absinthe-upload-link'

const file = new ReactNativeFile({
  uri: '…',
  type: 'image/jpeg',
  name: 'photo.jpg'
})

const files = ReactNativeFile.list([
  {
    uri: '…',
    type: 'image/jpeg',
    name: 'photo-1.jpg'
  },
  {
    uri: '…',
    type: 'image/jpeg',
    name: 'photo-2.jpg'
  }
])

License

MIT (see LICENSE)

Acknowledgements

apollo-absinthe-upload-link's People

Contributors

arielfarias avatar ckruse avatar dependabot[bot] avatar haodt avatar ihorkatkov avatar kooler62 avatar leandrocp avatar letops avatar mikaak avatar radzserg avatar topaxi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

apollo-absinthe-upload-link's Issues

Use native `fetch` by default / make rxjs optional

The readme says:

You can use the fetch option when creating an apollo-absinthe-upload-link to do a lot of custom networking.

Which made me think that if I don't use custom fetch, then browser's native fetch would be used. That is not the case and instead ajax from rxjs is being used if custom fetch is not provided. This has two major problems:

  1. Observable returned when rxjs is used does not have the same interface as a regular Observable (I am not sure how this is happening, but see the implications below).
  2. apollo-absinthe-upload-link's size is over 50KB gzipped, over 48KB being rxjs.

When it comes to number 1, composing with other links can be problematic. Let's say we have another link like this:

const logTimeLink = new ApolloLink((operation, forward) => {
  return forward(operation).map((data) => {
    // data from a previous link
    const time = new Date() - operation.getContext().start;
    console.log(`operation ${operation.operationName} took ${time} to complete`);
    return data;
  })
});

(https://www.apollographql.com/docs/link/overview.html)

Then if we try to use it together with apollo-absinthe-upload-link like this:

ApolloLink.from([logTimeLink, apolloAbsintheUploadLink])

we will get the following error:

Error: Network error: forward(...).map is not a function

Since the app I'm working on is compatible only with modern browsers, I work around this by initializing the link like this:

const apolloAbsintheUploadLink = createLink({ fetch, uri: '/graphql' });

This skips the whole rxjs code path, which brings me to point number 2. I am including 48KB of dead code:

screen shot 2018-12-05 at 4 04 32 pm

My proposal is to by default use browser's native fetch and remove rxjs as a dependency. It can be mentioned in the readme that if someone needs compatibility with older browsers, they can provide a custom fetch function.

How to use with batch-http?

Is it possible to use https://www.npmjs.com/package/apollo-link-batch-http with this apollo-absinthe-upload-link?

At now I have this setup:

const client = new ApolloClient({
  link: ApolloLink.from([
    new ApolloLink((operation, forward) => {
      return forward!(operation).map(response => {
        return {
          ...response,
          // tslint:disable-next-line:no-any
          data: (response as { payload: { data: any } }).payload.data
        };
      });
    }),
    new BatchHttpLink({
      uri: API_URL,
      credentials: "include"
    })
  ]),
  cache: inMemoryCache,
  resolvers: {}
});

Documentation/Examples on how to perform an upload

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

Missing headers

Hey,

I set up my Apollo client like so:

import ApolloClient from 'apollo-client'
import { setContext } from 'apollo-link-context'
import { concat } from 'apollo-link'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { createLink } from "apollo-absinthe-upload-link"

const httpLink = createLink({ uri: `${process.env.config.API_BASE_URL}/v2` })

const authLink = setContext(() => {
  let token = null
  // get the authentication token from local storage if it exists
  if (localStorage.getItem('auth')) {
    token = JSON.parse(localStorage.getItem('auth')).auth.token
  }
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      'x-spree-token': token ? `${token}` : "",
    }
  }
})

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
})

But in my query there is no x-spree-token header.
If I set it up with a regular Apollo HttpLink this works fine (except for the whole file upload part, obviously :P)

Release with credentials

Just letting you guys know I was smashing my head into the credentials issue because 1.5.0 on NPM does not include the credentials changes last year. It would be nice to see a new version on NPM that includes this.

Just wanted to bring awareness in case anyone else gets stuck on this as well.

Thanks for the awesome package!

Edit:
Note: I just added

"apollo-absinthe-upload-link": "git+https://github.com/bytewitchcraft/apollo-absinthe-upload-link.git#6468dc396bc0cd48b611950963ed90c55c506472",

to depedencies and it works fine.

Babel loader issue?

Hey,

Thanks so much for the recent updates, great work 👍

I can't run the newest version with my project though. This might be an issue on my side (still debugging this), but I think it comes from the module:

ERROR in ./~/apollo-absinthe-upload-link/src/index.js
Module parse failed: .../node_modules/apollo-absinthe-upload-link/src/index.js Unexpected token (24:21)
You may need an appropriate loader to handle this file type.
|           uri,
|           body: formData,
|           headers: { ...contextHeaders, ...headers },
|         })
|       }
 @ ./src/apollo-client.js 15:32-70
 @ ./src/index.js
 @ multi (webpack)-dev-server/client?http://0.0.0.0:5000 ./src/index.js

I exclude the node_modules in my own babel/webpack loading logic so i don't think it comes from my setup.

Cheers

apollo-client 2.4.10 no longer exports printAST

apollo-client no longer exports printAST. From the ChangeLog:

The apollo-client package no longer exports a printAST function from graphql/language/printer. If you need this functionality, import it directly: import { print } from "graphql/language/printer"

This causes apollo-absinthe-upload-link to fail:

./node_modules/apollo-absinthe-upload-link/src/index.js                                                                 │ [208] ./js/dialer.js 4.21 kB {0} [built]
Attempted import error: 'printAST' is not exported from 'apollo-client'.   

Mutation's context.fetchOptions not being added to actual fetch event.

In trying to add progress bars to file uploads, I was led to this comment, where a solution is proposed that uses a fetch wrapper for xhr to provide hooks for progress events.

  const httpLink = createUploadLink({
    uri: API_URL,
    fetch: buildAxiosFetch(axios, (config, input, init) => ({
      ...config,
      onUploadProgress: init.onUploadProgress,
    })),
  });

These hooks fire an event that is added to the fetch options, called onUploadProgress.

apolloClient.mutate({
  mutation: UPLOAD_FILE,
  variables: { file },
  context: {
    fetchOptions: {
      onUploadProgress: (progress => {
        console.info(progress);
      }),
    }
  },
});

The problem is that in apollo-absinthe-upload-link, the context.fetchOptions from a mutation are not being added to the fetch.

Error: Network error: Object(...) is not a function

Hi my mutation format looks like this:

import gql from 'graphql-tag'

export default (
  apolloClient,
  file,
) => {
  apolloClient
    .mutate({
      mutation: gql`
        mutation createGroup($file: Upload!) {
          createGroup(input: {file: $file}) {
            id
          }
        }
      `,
      variables: { file }
    })
}

I have an issue with sending my form data through the network, my error looks like this:
screen shot 2019-01-27 at 3 17 35 pm

Here is how I initialize my apollo client:

  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: ApolloLink.from([
      onError(({ graphQLErrors, networkError }) => {
        if (graphQLErrors) {
          graphQLErrors.map(({ message, locations, path }) => console.log(
            `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
          ));
        }
        if (networkError) {
          console.log(`[Network error]: ${networkError}`);
        }
      }),
      authLink,
      uploadLink
    ]),
    cache: new InMemoryCache().restore(initialState || {})
  })

Where my authLink looks like this:

  const authLink = setContext((_, { headers }) => {
    const token = getToken()
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : ''
      }
    }
  })

And my uploadLink looks like this:

  const uploadLink = createUploadLink({
    uri: '/api/graphql',
    credentials: 'same-origin'
  })

Both my api and client are sitting behind an nginx server that routes the relevant requests to their specific endpoints. Is there any nginx setup i need to do to get this working or is this more of either a frontend setup issue or an api setup issue

Honestly not sure what is wrong, I could do my other mutations just fine

Context headers not being passed

When I make a request with a file, it goes to the createUploadMiddleware, and I don't have my authentication header on that request.

In the image below: the 1st request has the authentication token, the second one, being the one with the file, doesn't.

screen shot 2018-08-21 at 11 59 36 am

Related code

  apolloLink() {
    return this.authContext()
      .concat(createUploadMiddleware({ uri: ENV.apiHost }))
      .concat(this.httpLink.create({ uri: ENV.apiHost }))
  }

  authContext = () => (
    setContext(async (_operation, _prevContext) => {
      const {token} = await this.customer.current()
      let headers = new HttpHeaders()
      if (token) {
        headers = headers.set('authorization', `Bearer ${token}`)
        return { headers }
      }
      return {}
    })
  )

I cloned the repo and saw the tests, they look fine, however this is happening.

Any clue why?

File upload disappears during transit over network

Hi! I am using

  • absinthe v1.4.0, apollo-client v2.50
  • react-apollo v2.0
  • apollo-absinthe-upload-link v1.5.0.

I'm having an issue where the file data disappears somewhere between calling an upload mutation and being sent over the network. I have a mutation to upload files:

const UPLOAD_FILE = gql`
  mutation uploadFile($classroomId: ID!, $upload: Upload!) {
    newFile: uploadFile(classroomId: $classroomId, upload: $upload) {
      id
      uuid
      url
    }
  }
`;

and I have a function to handle file uploads:

  handleUpload = async (uploads: Object[]) => {
    const upload = uploads[0];
    if (!this.validateUpload(upload)) return;

    const { classroom, uploadFile } = this.props;
    const variables = {
      classroomId: classroom.id,
      upload
    };

    try {
      console.log(variables);
      const {
        data: { newFile }
      } = await uploadFile({ variables });
      this.uploadCompleted(newFile);
      return;
    } catch (err) {
      console.log(err);
      this.handleErrors(err);
    }
  };

when I trigger the handleFileUpload function, I can see that the file exists in the variables object (I call console.log on the object in the code above), but I get the following GraphQL error:

Error: "GraphQL error: Argument "upload" has invalid value $upload."

Inspecting the network request, I noticed that the upload variable is just an empty object:

{
    "operationName": "uploadFile",
    "query": "mutation uploadFile($classroomId: ID!, $upload: Upload!) {\n  newFile: uploadFile(classroomId: $classroomId, upload: $upload) {\n    id\n    uuid\n    url\n    __typename\n  }\n}\n",
    "variables": {
      "classroomId": "xxxxxxx",
      "upload": {} #file data should be here
    }
  }

What might be causing this? Has this happened for anyone else?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.