GraphQL File Upload

All implementations and extensions are based on graphql-multipart-request-spec

Client

ApolloClient Setup

Client is using apollo-upload-client which implemented graphql-multipart-request-spec

Replace HttpLink with createUploadLink

Those two do the same thing, feel free to replace it!

import {
    ApolloClient,
    InMemoryCache
} from '@apollo/client';
import {
    createUploadLink
} from 'apollo-upload-client';

const client = new ApolloClient(config);

Add Scalar

Upload scalar

Due to different dependencies, this may cause some error, see Troubleshooting below

Add Schema

type Mutation {
    singleUpload(file: Upload!): File!
}

type File {
    filename: String!
    mimetype: String!
    encoding: String!
}

Frontend

Provide an input or use other frontend components to select a file:

<input type="file" onchange={fileUpload}>

Then validate the selected file:

const uploadOnChange = async (files: File[]) => {
    if (files.length === 0) return
    if (files.filter((file) => file.size > 10 * 1024 * 1024).length > 0) {
        /* throw error: file size exceed */
        return
    }
    if (
        files.filter(
            (file) => [ `image/png` , `image/jpeg` ].findIndex(
                (type) => file.type === type
            ) === -1
        ).length > 0
    ) {
        /* throw error: unacceptable file type */
        return
    }

    /* trigger mutation here */
}

Backend

GraphQL port it to assets server

If you are using Javascript, skip the import of graphql-upload

If you are using Typescript, you can use graphql-upload for type check, which implemented graphql-multipart-request-spec

import { FileUpload } from "graphql-upload";

const uploadFile = async (filePromise: {
  file: FileUpload;
}): Promise<boolean> => {
  try {
    const { file }: { file: FileUpload } = await filePromise;
    const fileReadStream = file.createReadStream(); // get the file readstream 

    /* ----------------------------- `*/
    /* Option 1: You can save the file on current server */
    /* const writeStream = fs.createWriteStream('fakepath/output.png') */
    /* Convert stream to file */
    /* readStream.pipe(writeStream) */

    /* ----------------------------- */
    /* Option 2: You can port the file to assets server if you need */
    const formData = new FormData();
    formData.append("attachmentData", fileReadStream, file.filename);

    await http.post( `assetsServer/fileUpload` , formData, {
      headers: {
        ...formData.getHeaders(),
      },
      timeout: 30000,
    });
    return true;
  } catch (error) {
    return false;
  }
};

const resolvers = {
  Query: {
    files: () => {
      // Return the record of files uploaded from your DB or API or filesystem.
    }
  },
  Mutation: {
    uploadFile
  },
};

Troubleshooting

There can be only one type named "Upload"

Possibly you included one lib which ALREADY implemented Upload Type, so you just need to delete scalar Upload

Unknown type "Upload". Did you mean "Float"?

You forget to add the scalar Upload

scalar Upload always causes error :(

  • If I add it -> Error: There can be only one type named "Upload"
  • If I remove it -> Error: Unknown type "Upload". Did you mean "Float"? Oh you got some tricky dependencies.

Try use other names like:

scalar FileUpload

That may help your issue, GraphQL may regard it as custom scalar.

createReadStream() crashes-RangeError: Maximum call stack size exceeded

RangeError: Maximum call stack size exceeded
        at _openReadFs (internal/fs/streams.js:1:1) 

This is due to outdated dependency of fs-capacitor .

To prevent future compatibility issue, set resolutions in package.json :

"resolutions": {
  "graphql-upload": "11.0.0"
},

Be aware that resolutions property is currently only handled by yarn package manager, not by npm

with npm, you have to preinstall an aditionnal module to force resolutions :

"scripts": {
  "preinstall": "npx npm-force-resolutions",
}

References