Apollo-server: Server 2.0: Data Sources

Created on 21 Jun 2018  ·  39Comments  ·  Source: apollographql/apollo-server

Hello,
I’m trying to use the new dataSourcesoption passed to ApolloServer 2.0.0-rc.0 using the RESTDataSource, though I currently do not see that option in the API reference.

When I do pass it to new ApolloServer as the documentation says,
dataSources: () => { return {moviesAPI: new MoviesAPI()};}
the dataSources does get added to the context, but ONLY if I return an empty {}, [], true/false, any number or null. If I return anything other than "empty", like an object containing {theAPI: new SomeRESTDataSource()} the dataSourcesfunction runs but dies there. The resolver that was called never runs and only receive a 400 response.

Thanks!

documentation 🙏 help-wanted

Most helpful comment

@martijnwalraven @jkdowdle Updated Babel to v. 7.0 (still was using 6), added node version to the config and now it works.

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

All 39 comments

I am having the same issue ! Did anybody got the full example with the dataSources and Apollo Server 2.0 to work?

I think I am having a similar issue, or maybe it is a little different.

import { ApolloServer } from 'apollo-server'
import { RESTDataSource } from 'apollo-datasource-rest'

import typeDefs from './schema'
import resolvers from './resolvers'

class PeopleAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = 'http://localhost:5000/api'
  }

  async find() {
    return this.get('people')
  }

  async findOne(id) {
    return this.get(`people/${id}`)
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => {
    return {
      people: new PeopleAPI()
    }
  }
})

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`)
})

This error is not in the node console, but in the graphql network playground response.

{
  "errors": [
    {
      "message":
        "Class constructor RESTDataSource cannot be invoked without 'new'",
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "TypeError: Class constructor RESTDataSource cannot be invoked without 'new'",
            "    at new PeopleAPI (/home/joshd/sandbox/data-sources/src/server.js:8:17)",
            "    at Object.dataSources (/home/joshd/sandbox/data-sources/src/server.js:27:15)",
            "    at Object.<anonymous> (/home/joshd/sandbox/data-sources/node_modules/apollo-server-core/src/runHttpQuery.ts:289:43)",
            "    at Generator.next (<anonymous>)",
            "    at /home/joshd/sandbox/data-sources/node_modules/apollo-server-core/dist/runHttpQuery.js:7:71",
            "    at new Promise (<anonymous>)",
            "    at __awaiter (/home/joshd/sandbox/data-sources/node_modules/apollo-server-core/dist/runHttpQuery.js:3:12)",
            "    at requestPayload.map (/home/joshd/sandbox/data-sources/node_modules/apollo-server-core/src/runHttpQuery.ts:145:61)",
            "    at Array.map (<anonymous>)",
            "    at Object.<anonymous> (/home/joshd/sandbox/data-sources/node_modules/apollo-server-core/src/runHttpQuery.ts:145:35)"
          ]
        }
      }
    }
  ]
}

Thank you for flagging the issues. Here is an example of data sources https://glitch.com/edit/#!/as2-subscriptions-datasources. There are some issues that came up when building it out: #1219 and #1218

@martijnwalraven might have some more information

@jkdowdle It looks like there are some other issues around extending classes in your example. What are you using to transpile?

@evans Ah good point, I didn't think to check that.

I used a launchpad for a starting point so it is using nodemon and some babel presets. Tomorrow I will check if I can fix it by changing the babel config.

Thank you.

@xersist @FLoppix Could you elaborate on the issue you're seeing? What does your new ApolloServer call look like?

@martijnwalraven Below is based off of @jkdowdle/@evans code. Yes, the code seems to work in the playground. :-/

const http = require('http');
const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');
const { RESTDataSource } = require('apollo-datasource-rest')

class PeopleAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = 'http://localhost:5000/api'
  }

  async find() {
    return this.get('people')
  }
}

const typeDefs = gql`
    type Query {
        people: [String]
    }
`;

const resolvers = {
  Query: {
    people: (_parent, _args, { dataSources }) => {
      dataSources.people.find().then(p => {
        console.log(p)
        console.log(typeof p)
      });
      return dataSources.people.find().then(JSON.parse);
    }
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => {
    return {
      people: new PeopleAPI()
    };
  },
});

const app = express();

server.applyMiddleware({ app, path: '/' });

const httpServer = http.createServer(app);

httpServer.listen({ port: 4000 }, () =>
  console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);

After upgrading to rc.2 today, I still have the issue though now I am recieving the following error in the console.
```$xslt
TypeError: Cannot read property 'calculateHttpHeaders' of undefined
at Object. (\node_modulesapollo-server-coresrc\runHttpQuery.ts:402:20)
at Generator.next ()
at fulfilled (\node_modulesapollo-server-core\dist\runHttpQuery.js:4:58)
at process._tickCallback (internal/process/next_tick.js:68:7)

and this error in the playground

{
"error": "Unexpected token < in JSON at position 0"
}

I simply have to comment out ```// people: new PeopleAPI()``` for the rest of the app to work.

I had more ```presets``` in my ```.babelrc``` file but currently only ```env```

Below are the packages I am currently using, and my original presets I've used, latest, stage-2 & 3.

"dependencies": {
"apollo-datasource-rest": "^2.0.0-rc.2",
"apollo-server": "^2.0.0-rc.2",
"apollo-server-express": "^2.0.0-rc.2",
"express": "^4.16.3",
"graphql": "^0.13.2"
},
```

I’m not at my computer right now, but the people resolver seems off. You’re making two requests and only returning (as a promise) one of them. And you shouldn’t have to call JSON.parse manually. What does your API return? Make sure to set the content type to application/json.

LOL, sorry about that, it was just in @evans original code. The REST service I am using does return JSON. Also I changed the rest-server.js in the glitch code to return application/json as well.
Though before even attempting to even run a query I get those errors. Even the Introspection fails in the playground. I even added the trailing slash on the baseURL just-to-see.
Just a screenshot of what is seen
image

Again, this is in a project locally.

Actually, once updating to [email protected] and [email protected]

I too am getting the same error

TypeError: Cannot read property 'calculateHttpHeaders' of undefined
    at Object.<anonymous> (/home/joshd/sandbox/data-sources/node_modules/apollo-server-core/src/runHttpQuery.ts:402:20)
    at Generator.next (<anonymous>)
    at fulfilled (/home/joshd/sandbox/data-sources/node_modules/apollo-server-core/dist/runHttpQuery.js:4:58)
    at process._tickCallback (internal/process/next_tick.js:68:7)

Here is a minimal repo if it is helpful. The rest server is in there under /rest but I too get the error before making any requests.

https://github.com/jkdowdle/datasource-issue

I'll keep looking and see if I can find anything else

Ok so that error will go away once I stop using babel-node. So just running nodemon node ./src/server.js the error goes away and I am able to make a query, however the query never hits the rest endpoint and throws a 404 error.

Data Srouce

  constructor() {
    super()
    this.baseURL = 'http://localhost:5001/api'
  }

  async find() {
    try {
      return this.get('people')
    } catch (e) {
      throw new Error(e)
    }
  }

I am trying to hit 'http://localhost:5001/api/people but it seems like the baseURL only goes with the domain name and not the path after words so if I change the call to this.get to include api there then it works.

  async find() {
    return this.get('api/people')
  }

Is this as expected?

Is this as expected?

Ok I guess I didn't realize that under the hood it's using new URL(...) and I didn't know how that worked. Maybe we could clarify that in the docs? Do you guys think that would be helpful to others?

@xersist without using babel, does removing '/api' from the baseURL property on the class fix your issue?

@jkdowdle Yes, that fixes the issue. As long as I don't use nodemon ./server.js --exec babel-node on this test project it works.
So I'm assuming something to do with the way babel transpiles the code for something in the apollo-core runHttpQuery.js?? Changing 1 line to an import statement of course causes an error.

@jkdowdle Yeah, the path you pass to get is treated as a relative URL, so that means baseURL should be http://localhost:5001/api/ (note the trailing slash). Maybe we should add that slash automatically if it isn't there, because this will be a common gotcha.

@xersist I think you're right. Though, I am not sure how to enable es modules. My understanding is that babel-preset-env is an all in one?

@martijnwalraven Are there also resources or documentation to handle post request?

I discovered that get(), post() are wrappers around fech() with some caching: https://github.com/apollographql/apollo-server/blob/version-2/packages/apollo-datasource-rest/src/RESTDataSource.ts

The post method is:
protected async post( path: string, params?: Params, options?: RequestInit, ): Promise<any> { return this.fetch(path, params, Object.assign({ method: 'POST' }, options)); }

I’ll add the documentation for this. Thanks Andrei for helping out.

On Mon, Jun 25, 2018 at 10:32 AM Andrei Drynov notifications@github.com
wrote:

I discovered that get(), post() are wrappers around fech() with some
caching:
https://github.com/apollographql/apollo-server/blob/version-2/packages/apollo-datasource-rest/src/RESTDataSource.ts

The post method is:

protected async post(
path: string,
params?: Params,
options?: RequestInit,
): Promise {
return this.fetch(path, params, Object.assign({ method: 'POST' }, options));
}


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/apollographql/apollo-server/issues/1217#issuecomment-399890955,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACz20XAqXNzYoFrByOznQNObW7Cdl0dVks5uAK4kgaJpZM4UyKb6
.

>

Best Regards,

Prosper Otemuyiwa
Principal Developer Advocate
(c) +234-708-909-5645 | +254-735-589-607 | @unicodeveloper
https://twitter.com/unicodeveloper | prosper.otemuyiwa1 ( Skype )

One missing piece here (I just realized) is the ability to pass in an object and have it be serialized as a JSON body automatically. Currently, you have to manually pass headers and body as options (which is of type RequestInit and passed through to fetch).

Martin,

I followed this.post() and this.fetch methods to post

 const data = { login: username, password, };

 const options: RequestInit = {
        body: JSON.stringify(data)
  };

    const result = await this.post('users/login/mobile', null, options);

but it fails with TypeError: this.keyValueCache.get is not a function with request.Request.body = null

@GlobeTechGit Hmmm, that's weird, I just tested this. Can you see what line this fails on? What do you mean by 'with request.Request.body = null'?

My bad, used "cache: true" instead of "cacheControl: true" in Apollo Server constructor.

The above works great. Thank you for your hard work.

@GlobeTechGit Thanks, good to hear!

@adrynov @GlobeTechGit: I added more convenient support for passing objects as bodies to POST, PATCH and PUT requests in https://github.com/apollographql/apollo-server/commit/b6e2096876b164030ad4c7e1a34d522138ee4cae.

@martijnwalraven Now it works perfectly for me after your latest commit :) Thanks!
But I also have a problem with Class constructor RESTDataSource cannot be invoked without 'new' and it's because of babel-node for sure. It just works fine without it.

@yakovlevyuri This seems to be a known Babel issue, because ES6 classes cannot be extended with ES5-style code. Since Apollo Server currently targets Node 6 and up, which supports native classes, you should adjust your Babel preset accordingly. I'm not too familiar with Babel, but it seems the recommended way is to use babel-preset-env.

@martijnwalraven @jkdowdle Updated Babel to v. 7.0 (still was using 6), added node version to the config and now it works.

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

@evans Thanks for your working example of dataSources. I haven't been able to get it to work at all and can't find working examples except for yours.

@rgstephens What issues are you running into? Have you seen the docs? I realize they need improvement, and I'll be working on that, but knowing which parts are confusing or incomplete would be very helpful.

I couldn't figure out how to set the authorization header. Thanks for the pointer to the docs. With a slight modification it worked for me:

request.headers.set('Authorization', 'Bearer ' + this.context.token);

Any updates on this? I also have this error

Class constructor RESTDataSource cannot be invoked without 'new'                                                                   

it seems like RESTDataSource is not transformed by babel? https://github.com/babel/babel/issues/7022#issuecomment-351582197

@davidalekna do you have details about the node version and babel version you are using? If so I'm willing to try and look more.

I just followed the following two links and was not able to reproduce the issue:

  1. https://www.apollographql.com/docs/apollo-server/getting-started.html
  2. https://www.apollographql.com/docs/apollo-server/features/data-sources.html
issue-1217:$ node -v
v10.11.0
issue-1217:$ npm -v
6.4.1

Example: https://glitch.com/edit/#!/diligent-reaper?path=server.js:67:2

package.json

{
  "name": "issue-1217",
  "version": "1.0.0",
  "license": "ISC",
  "dependencies": {
    "apollo-datasource-rest": "^0.1.5",
    "apollo-server": "^2.1.0",
    "graphql": "^14.0.2"
  }
}

index.js

const { ApolloServer, gql } = require('apollo-server');
const { RESTDataSource } = require('apollo-datasource-rest');

function parseSwapiURL(url) {
  // "https://swapi.co/api/people/1/"
  const parts = url.split('/');
  return {
    protocol: parts[0],
    domain: parts[2],
    model: parts[4],
    id: parts[5],
  };
}

class StarWarsAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://swapi.co/api/';
  }

  async getPeople() {
    const response = await this.get(`people`);
    return response.results;
  }

  async getFilm(id) {
    return this.get(`films/${id}`);
  }
}

const typeDefs = gql`
  type Person {
    name: String
    films: [Film]
  } 
  type Film {
    title: String
  }
  type Query {
    people: [Person]
  }
`;

const resolvers = {
  Query: {
    people:  (parent, args, context) => {
      return context.dataSources.starWars.getPeople();
    }
  },
  Person: {
    films: async (parent, args, context) => {
      return Promise.all(
        parent.films
          .map((url) => parseSwapiURL(url).id)
          .map((id) => context.dataSources.starWars.getFilm(id))
      );
    }
  }
};

const server = new ApolloServer({
    typeDefs,
    resolvers,
    dataSources: () => {
      return {
        starWars: new StarWarsAPI(),
      };
    },
});

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

@m14t I created a repo here and adjusted your example to apollo-server-express with webpack. When dataSources are commented out it works. If you uncomment it then it won't work. Try to call StarWarsAPI directly not from resolver and you'll get the error about the new keyword.

const test = new StarWarsAPI()
test().getPeople()

node: 10.11.0
babel: ^7

@davidalekna i was running into the same issue as you were, however, i am using babel6.

anyways i found that the issue was the babel-preset-env plugin an my environment. it seems necessary to configure it with the node target in babel6 this would be ["env", {"targets": {"node": "current"}}]. so you might want to try adding a targets in your config for that plugin.

@davidalekna i was running into the same issue as you were, however, i am using babel6.

anyways i found that the issue was the babel-preset-env plugin an my environment. it seems necessary to configure it with the node target in babel6 this would be ["env", {"targets": {"node": "current"}}]. so you might want to try adding a targets in your config for that plugin.

thanks man, will give it a try! :)

@martijnwalraven @jkdowdle Updated Babel to v. 7.0 (still was using 6), added node version to the config and now it works.

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

This worked for me with latest @babel/env. Saved me after hours of searching

Using NextJS 7 and up? Add this line to your babel.config.js

presets: [['next/babel', { 'preset-env': { 'targets': { 'node': 'current' } } }]],

Looks like people have figured out solutions here so I'm going to close this in favor of new issues being opened for specific issues! Thanks for the great back and forth everyone!

Was this page helpful?
0 / 5 - 0 ratings