Search code examples
node.jsfilegraphqlapollo-servergql

NodeJS create and POST file with GQL


I've been unable to figure out in NodeJS how to:

  1. create a "file" in memory from a raw string; and,
  2. how to POST that data to another server that expects a multipart/form-data payload.

Seems you cannot use the Blob or File classes in NodeJS. I've read the pattern should be to use the Buffer class. I still cannot get it to work with Buffers.

My GQL Datasoruce class looks something like:

const { RESTDataSource } = require('apollo-datasource-rest');
const FormData = require('form-data');

export default class MyDatasource extends RESTDataSource {

  async postFileToServer({ string }) {

    const inMemoryFile = Buffer.from(string, 'utf-8');

    const myForm = new FormData();
    myForm.append('file', inMemoryFile, 'file.txt');

    const url = 'http://examnple.com';
    const opts = { headers: { 'Content-Type': 'multipart/form-data' } };

    return await this.post(url, myForm, opts);
  }
}

The endpoint I want to hit works fine when I use Postman to make the API call with a file from my local machine. However, I need the GQL server to create the file from a raw string to afterwards call the example.com endpoint that is expecting a multipart/form-data.

The above example code always gives me an error of Status 400 and SyntaxError: Unexpected token - in JSON at position 0


Solution

  • File upload works for me using the apollo-datasource-rest package. Here is an example:

    server.ts:

    import { ApolloServer, gql } from 'apollo-server';
    import MyDatasource from './datasource';
    
    const typeDefs = gql`
      type Query {
        dummy: String
      }
      type Mutation {
        upload: String
      }
    `;
    const resolvers = {
      Mutation: {
        upload(_, __, { dataSources }) {
          return dataSources.uploadAPI.postFileToServer({ str: '1234' });
        },
      },
    };
    
    const server = new ApolloServer({
      typeDefs,
      resolvers,
      dataSources: () => {
        return {
          uploadAPI: new MyDatasource(),
        };
      },
    });
    const port = 3001;
    server.listen(port).then(({ url }) => console.log(`🚀 Server ready at ${url}`));
    

    datasource.ts:

    import { RESTDataSource } from 'apollo-datasource-rest';
    import FormData from 'form-data';
    
    export default class MyDatasource extends RESTDataSource {
      public async postFileToServer({ str }) {
        const inMemoryFile = Buffer.from(str, 'utf-8');
        const myForm = new FormData();
        myForm.append('file', inMemoryFile, 'file.txt');
        const url = 'http://localhost:3000/upload';
    
        return this.post(url, myForm);
      }
    }
    

    uploadServer.ts:

    import multer from 'multer';
    import express from 'express';
    import path from 'path';
    
    const upload = multer({ dest: path.resolve(__dirname, 'uploads/') });
    const app = express();
    const port = 3000;
    
    app.post('/upload', upload.single('file'), (req, res) => {
      console.log(req.file);
      console.log(req.body);
      res.sendStatus(200);
    });
    
    app.listen(port, () => {
      console.log(`upload server is listening on http://localhost:${port}`);
    });
    

    The logs printed in the controller of /upload API:

    {
      fieldname: 'file',
      originalname: 'file.txt',
      encoding: '7bit',
      mimetype: 'text/plain',
      destination: '/Users/ldu020/workspace/github.com/mrdulin/apollo-graphql-tutorial/src/stackoverflow/63181608/uploads',
      filename: '3cba4dded6089479ad495e2fb2daac21',
      path: '/Users/ldu020/workspace/github.com/mrdulin/apollo-graphql-tutorial/src/stackoverflow/63181608/uploads/3cba4dded6089479ad495e2fb2daac21',
      size: 4
    }
    [Object: null prototype] {}
    

    source code: https://github.com/mrdulin/apollo-graphql-tutorial/tree/master/src/stackoverflow/63181608