Skip to content

Commit

Permalink
move to updated API design
Browse files Browse the repository at this point in the history
  • Loading branch information
James Baxley authored and evans committed May 11, 2018
1 parent a7a4e3d commit f3bb826
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 420 deletions.
18 changes: 9 additions & 9 deletions packages/apollo-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,31 @@
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"devDependencies": {
"@types/cors": "^2.8.3",
"@types/express": "^4.11.1",
"@types/graphql": "^0.12.7",
"@types/micro": "^7.3.1",
"@types/microrouter": "^2.2.2",
"@types/node": "^9.6.1",
"typescript": "2.8.1"
},
"peerDependencies": {
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0"
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
},
"dependencies": {
"@types/cors": "^2.8.3",
"@types/express": "^4.11.1",
"@types/node": "^9.6.1",
"apollo-engine": "^1.0.4",
"apollo-server-express": "^1.3.4",
"apollo-upload-server": "^5.0.0",
"apollo-server-micro": "^1.3.4",
"body-parser": "^1.18.2",
"cors": "^2.8.4",
"express": "^4.16.3",
"graphql-playground-middleware-express": "^1.6.0",
"graphql-subscriptions": "^0.5.8",
"graphql-tools": "^2.23.1",
"lodash.merge": "^4.6.1",
"stackman": "^3.0.1",
"micro": "^9.1.4",
"microrouter": "^3.1.1",
"subscriptions-transport-ws": "^0.9.7"
}
}
319 changes: 319 additions & 0 deletions packages/apollo-server/src/ApolloServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import { makeExecutableSchema, mergeSchemas } from 'graphql-tools';
import { Server as HttpServer } from 'http';

import {
execute,
GraphQLSchema,
subscribe,
DocumentNode,
print,
} from 'graphql';
import { ApolloEngine as Engine } from 'apollo-engine';
import { SubscriptionServer } from 'subscriptions-transport-ws';

import { CorsOptions } from 'cors';
import {
Config,
ListenOptions,
MiddlewareOptions,
MiddlewareRegistrationOptions,
ServerInfo,
Context,
ContextFunction,
} from './types';

import { formatError } from './errors';

// this makes it easy to get inline formatting and highlighting without
// actually doing any work
export const gql = String.raw;

export class ApolloServerBase<Server = HttpServer, Request = any> {
app?: Server;
schema: GraphQLSchema;
private context?: Context | ContextFunction;
private engine?: Engine;
private appCreated: boolean = false;
private middlewareRegistered: boolean = false;
private http?: HttpServer;
private subscriptions?: any;
private graphqlEndpoint: string = '/graphql';
private cors?: CorsOptions;

constructor(config: Config<Server>) {
const {
typeDefs,
resolvers,
schemaDirectives,
schema,
context,
app,
engine,
subscriptions,
cors,
} = config;

this.context = context;
this.schema = schema
? schema
: makeExecutableSchema({
typeDefs: Array.isArray(typeDefs)
? typeDefs.reduce((prev, next) => prev + '\n' + next)
: typeDefs,
schemaDirectives,
resolvers,
});

this.subscriptions = subscriptions;
this.cors = cors;

if (app) {
this.app = app;
} else {
this.app = this.createApp();
this.appCreated = true;
}

// only access this onces as its slower on node
const { ENGINE_API_KEY, ENGINE_CONFIG } = process.env;
const shouldLoadEngine = ENGINE_API_KEY || ENGINE_CONFIG;
if (engine === false && shouldLoadEngine) {
console.warn(
'engine is set to false when creating ApolloServer but either ENGINE_CONFIG or ENGINE_API_KEY were found in the environment',
);
}
if (engine !== false) {
// detect engine, and possibly load it
try {
const { ApolloEngine } = require('apollo-engine');
let engineConfig: any = {};
if (typeof engine === 'string') engineConfig.apiKey = engine;
// XXX this can be removed if / when engine does this automatically
if (typeof engine === 'boolean' || typeof engine === 'undefined') {
if (ENGINE_API_KEY) engineConfig.apiKey = ENGINE_API_KEY;
if (!ENGINE_API_KEY) {
engineConfig.apiKey = 'engine:local:01';
engineConfig.reporting = { disabled: true };
}
}
// yeah this isn't great, should replace with real check maybe?
if (typeof engine === 'object') {
engineConfig = { ...engine };
}
this.engine = new ApolloEngine(engineConfig);
} catch (e) {
if (shouldLoadEngine) {
console.warn(`ApolloServer was unable to load Apollo Engine and found environment variables that seem like you want it to be running? To fix this, run the following command:
npm install apollo-engine --save
`);
}
}
}
}

public applyMiddleware(opts: MiddlewareOptions = {}) {
if (this.appCreated) {
throw new Error(`It looks like server.applyMiddleware was called when app was not passed into ApolloServer. To use middlware, you need to create an ApolloServer from a variant package and pass in your app:
const { ApolloServer } = require('apollo-server/express');
const express = require('express');
const app = express();
const server = new ApolloServer({ app, resolvers, typeDefs });
// then when you want to add the middleware
server.applyMiddleware();
// then start the server
server.listen()
`);
}
const registerOptions: MiddlewareRegistrationOptions<Server, Request> = {
endpoint: this.graphqlEndpoint,
graphiql: '/graphiql',
cors: this.cors,
...opts,
app: this.app,
request: this.request.bind(this),
};
this.graphqlEndpoint = registerOptions.endpoint;
// this function can either mutate the app (normal)
// or some frameworks maj need to return a new one
const possiblyNewServer = this.registerMiddleware(registerOptions);
this.middlewareRegistered = true;
if (possiblyNewServer) this.app = possiblyNewServer;
}

public listen(opts: ListenOptions, listenCallback?: (ServerInfo) => void) {
if (!this.appCreated && !this.middlewareRegistered) {
throw new Error(
`It looks like you are trying to run ApolloServer without applying the middleware. This error is thrown when using a variant of ApolloServer (i.e. require('apollo-server/variant')) and passing in a custom app. To fix this, before you call server.listen, you need to call server.applyMiddleware():
const app = express();
const server = new ApolloServer({ app, resolvers, typeDefs });
// XXX this part is missing currently!
server.applyMiddleware();
server.listen();
`,
);
}
if (!opts) {
opts = {};
listenCallback = this.defaultListenCallback;
}
if (typeof opts === 'function') {
listenCallback = opts;
opts = {};
}

if (!listenCallback) listenCallback = this.defaultListenCallback;
const options = {
port: process.env.PORT || 4000,
...opts,
};

this.http = this.getHttpServer(this.app);
if (this.subscriptions !== false) {
const config =
this.subscriptions === true || typeof this.subscriptions === 'undefined'
? {
path: this.graphqlEndpoint,
}
: this.subscriptions;
this.createSubscriptionServer(this.http, config);
}

if (this.engine) {
this.engine.listen({ port: options.port, httpServer: this.http }, () => {
listenCallback(this.engine.engineListeningAddress);
});
return;
}

this.http.listen(options.port, listenCallback);
}

public async stop() {
if (this.engine) await this.engine.stop();
if (this.http) await new Promise(s => this.http.close(s));
}

private createSubscriptionServer(server: HttpServer, config) {
const { onDisconnect, onOperation, onConnect, keepAlive, path } = config;
SubscriptionServer.create(
{
schema: this.schema,
execute,
subscribe,
onConnect: onConnect
? onConnect
: (connectionParams, webSocket) => ({ ...connectionParams }),
onDisconnect: onDisconnect,
onOperation: async (message, connection, webSocket) => {
connection.formatResponse = value => ({
...value,
errors: value.errors && value.errors.map(formatError),
});
let context: Context = this.context ? this.context : { connection };

try {
context =
typeof this.context === 'function'
? await this.context({ connection })
: context;
} catch (e) {
console.error(e);
throw e;
}

return { ...connection, context };

This comment has been minimized.

Copy link
@jedwards1211

jedwards1211 Feb 13, 2019

Contributor

@clayne11 @evans the documentation website recommends using subscriptions.onConnect to set context:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  subscriptions: {
    onConnect: (connectionParams, webSocket) => {
      if (connectionParams.authToken) {
        return validateToken(connectionParams.authToken)
          .then(findUser(connectionParams.authToken))
          .then(user => {
            return {
              currentUser: user,
            };
          });
      }

      throw new Error('Missing auth token!');
    },
  },
});

But the object returned by onConnect, which is then stored as connection.context, gets completely overwritten here by the return value of calling ApolloServer.context({ connection }), and there is no documentation on the website of calling context this way. The only documentation shows it being called with (req, req)...seems like a big problem. This is causing #1597

This comment has been minimized.

Copy link
@jedwards1211

jedwards1211 Feb 13, 2019

Contributor

The connectionParams are not even accessible in the call to this.context (even in the latest code) so this.context doesn't even have the information it needs to construct a proper context for subscriptions.

This comment has been minimized.

Copy link
@jedwards1211

jedwards1211 Feb 14, 2019

Contributor

Okay I finally found a part of the docs that mentions returning connection.context from the context function.
However, other parts of the docs have code that assumes req will always be defined.

This has caused me a lot of confusion. I'd encourage you guys to reconsider this API change.
I'm not so sure it's helpful to call the context function for subscriptions, since fetch and subscriptions will be following completely different branches of code within it (hence might as well be completely separate functions). Probably better for users to call some shared function to create their context with parameters extracted from req or connectionParams, like this:

  async function createContext({ authToken }: { authToken: ?string }): Promise<GraphQLContext> {
    let userId = null
    if (authToken) {
      ({ user: { id: userId } } = await loginWithToken(null, authToken))
    }
    return {
      userId,
      sequelize,
    }
  }
  return new ApolloServer({
      schema,
      formatError,
      context: async ({ req, res }: {req: $Request, res: $Response}): Promise<GraphQLContext> => {
        const auth = req.get('authorization')
        const match = /^Bearer (.*)$/i.exec(auth || '')
        const authToken = match ? match[1] : null
        return createContext({ authToken })
      },
      subscriptions: {
        onConnect: async (connectionParams: any) => {
          const { authToken } = connectionParams
          return createContext({ authToken })
        },
      },
    })
},
keepAlive,
},
{
server,
path: path || this.graphqlEndpoint,
},
);
}

async request(request: Request) {
if (!this) {
throw new Error(`It looks like you tried to call this.request but didn't bind it to the parent class. To fix this,
when calling this.request, either call it using an error function, or bind it like so:
this.request.bind(this);
`);
}
let context: Context = this.context ? this.context : { request };

try {
context =
typeof this.context === 'function'
? await this.context({ req: request })
: context;
} catch (e) {
console.error(e);
throw e;
}

return {
schema: this.schema,
tracing: Boolean(this.engine),
cacheControl: Boolean(this.engine),
logFunction: this.logger,
formatError,
context,
};
}

private logger(...args) {
// console.log(...args);
}

private defaultListenCallback({ url }: { url?: string } = {}) {
console.log(
`ApolloServer is listening at ${url || 'http://localhost:4000'}`,
);
}

/* region: vanilla ApolloServer */
createApp(): Server {
throw new Error(`It looks like you called server.listen on an ApolloServer that is missing a server! This means that either you need to pass an external server when creating an ApolloServer, or use an ApolloServer variant that supports a default server:
const { ApolloServer } = require('apollo-server');
// or
const { ApolloServer } = require('apollo-server/express');
To see all supported servers, check the docs at https://apollographql.com/docs/server
`);
}

/* end region: vanilla ApolloServer */

/* region: variant ApolloServer */

registerMiddleware(
config: MiddlewareRegistrationOptions<Server, Request>,
): Server | void {
throw new Error(`It looks like you called server.addMiddleware on an ApolloServer that is missing a server! Make sure you pass in an app when creating a server:
const { ApolloServer } = require('apollo-server/express');
const express = require('express');
const app = express();
const server = new ApolloServer({ app, typeDefs, resolvers });
`);
}

getHttpServer(app: Server): HttpServer {
throw new Error(
`It looks like you are trying to use subscriptions with ApolloServer but we couldn't find an http server from your framework. To fix this, please open an issue for you variant at the apollographql/apollo-server repo`,
);
}

/* end region: variant ApolloServer */

closeApp(app: Server): Promise<void> | void {}
}
4 changes: 0 additions & 4 deletions packages/apollo-server/src/connector.ts

This file was deleted.

5 changes: 0 additions & 5 deletions packages/apollo-server/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// import * as stacks from 'stackman';

// // register in depth error logs
// const stackman = stacks();

export interface ExceptionDetails {
type?: string;
code?: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/apollo-server/src/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from 'graphql-tools';
export * from 'graphql-subscriptions';
export { gql } from './ApolloServer';
Loading

0 comments on commit f3bb826

Please sign in to comment.