mirror of
https://github.com/vale981/apollo-server
synced 2025-03-16 07:46:54 -04:00

* apollo-server-core: add file upload hooks and configuration * apollo-server-core: make requestOptions and fileUploadsConfig public fields in ApolloServerBase * apollo-server-express: initial fileuploads middleware * apollo-server-hapi: initial file uploads middleware * core: remove fileuploads from constructor and add merge capabilities * express: add fileuploads to registerServer and single test * hapi: add fileuploads schema merge to registerServer * express: add check for EPIPE and ignore it due to upload server issue * core: add scalar Upload to server construction
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import {
|
|
makeExecutableSchema,
|
|
addMockFunctionsToSchema,
|
|
IResolvers,
|
|
mergeSchemas,
|
|
} from 'graphql-tools';
|
|
import { Server as HttpServer } from 'http';
|
|
import {
|
|
execute,
|
|
GraphQLSchema,
|
|
subscribe,
|
|
ExecutionResult,
|
|
GraphQLError,
|
|
GraphQLResolveInfo,
|
|
ValidationContext,
|
|
FieldDefinitionNode,
|
|
} from 'graphql';
|
|
|
|
import { ApolloEngine } from 'apollo-engine';
|
|
import {
|
|
SubscriptionServer,
|
|
ExecutionParams,
|
|
} from 'subscriptions-transport-ws';
|
|
|
|
import { formatApolloErrors } from './errors';
|
|
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions';
|
|
import { LogFunction, LogAction, LogStep } from './logging';
|
|
|
|
import {
|
|
Config,
|
|
ListenOptions,
|
|
MiddlewareOptions,
|
|
RegistrationOptions,
|
|
ServerInfo,
|
|
Context,
|
|
ContextFunction,
|
|
SubscriptionServerOptions,
|
|
} from './types';
|
|
|
|
const NoIntrospection = (context: ValidationContext) => ({
|
|
Field(node: FieldDefinitionNode) {
|
|
if (node.name.value === '__schema' || node.name.value === '__type') {
|
|
context.reportError(
|
|
new GraphQLError(
|
|
'GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production',
|
|
[node],
|
|
),
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
export class ApolloServerBase<Request = RequestInit> {
|
|
public disableTools: boolean;
|
|
// set in the listen function if subscriptions are enabled
|
|
public subscriptionsPath: string;
|
|
public requestOptions: Partial<GraphQLOptions<any>>;
|
|
|
|
private schema: GraphQLSchema;
|
|
private context?: Context | ContextFunction;
|
|
private graphqlPath: string = '/graphql';
|
|
private engineProxy: ApolloEngine;
|
|
private engineEnabled: boolean = false;
|
|
|
|
private http?: HttpServer;
|
|
private subscriptionServer?: SubscriptionServer;
|
|
protected getHttp: () => HttpServer;
|
|
|
|
constructor(config: Config<Request>) {
|
|
const {
|
|
context,
|
|
resolvers,
|
|
schema,
|
|
schemaDirectives,
|
|
typeDefs,
|
|
introspection,
|
|
mocks,
|
|
...requestOptions
|
|
} = config;
|
|
|
|
//While reading process.env is slow, a server should only be constructed
|
|
//once per run, so we place the env check inside the constructor. If env
|
|
//should be used outside of the constructor context, place it as a private
|
|
//or protected field of the class instead of a global. Keeping the read in
|
|
//the contructor enables testing of different environments
|
|
const env = process.env.NODE_ENV;
|
|
const isDev = env !== 'production' && env !== 'test';
|
|
|
|
// if this is local dev, we want graphql gui and introspection to be turned on
|
|
// in production, you can manually turn these on by passing { introspection: true }
|
|
// to the constructor of ApolloServer
|
|
// we use this.disableTools to track this internally for later use when
|
|
// constructing middleware by frameworks
|
|
if (typeof introspection === 'boolean') this.disableTools = !introspection;
|
|
else this.disableTools = !isDev;
|
|
|
|
if (this.disableTools) {
|
|
const noIntro = [NoIntrospection];
|
|
requestOptions.validationRules = requestOptions.validationRules
|
|
? requestOptions.validationRules.concat(noIntro)
|
|
: noIntro;
|
|
}
|
|
|
|
this.requestOptions = requestOptions;
|
|
this.context = context;
|
|
|
|
const enhancedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
|
|
enhancedTypeDefs.push(`scalar Upload`);
|
|
|
|
this.schema = schema
|
|
? schema
|
|
: makeExecutableSchema({
|
|
typeDefs: enhancedTypeDefs.join('\n'),
|
|
schemaDirectives,
|
|
resolvers,
|
|
});
|
|
|
|
if (mocks) {
|
|
addMockFunctionsToSchema({
|
|
schema: this.schema,
|
|
preserveResolvers: true,
|
|
mocks: typeof mocks === 'boolean' ? {} : mocks,
|
|
});
|
|
}
|
|
}
|
|
|
|
public use({ getHttp, path }: RegistrationOptions) {
|
|
// we need to delay when we actually get the http server
|
|
// until we move into the listen function
|
|
this.getHttp = getHttp;
|
|
this.graphqlPath = path;
|
|
}
|
|
|
|
public enhanceSchema(
|
|
schema: GraphQLSchema | { typeDefs: string; resolvers: IResolvers },
|
|
) {
|
|
this.schema = mergeSchemas({
|
|
schemas: [
|
|
this.schema,
|
|
'typeDefs' in schema ? schema['typeDefs'] : schema,
|
|
],
|
|
resolvers: 'resolvers' in schema ? [, schema['resolvers']] : {},
|
|
});
|
|
}
|
|
|
|
public listen(opts: ListenOptions = {}): Promise<ServerInfo> {
|
|
this.http = this.getHttp();
|
|
|
|
const options = {
|
|
...opts,
|
|
http: {
|
|
port: process.env.PORT || 4000,
|
|
...opts.http,
|
|
},
|
|
};
|
|
|
|
if (opts.subscriptions !== false) {
|
|
let config: SubscriptionServerOptions;
|
|
if (
|
|
opts.subscriptions === true ||
|
|
typeof opts.subscriptions === 'undefined'
|
|
) {
|
|
config = {
|
|
path: this.graphqlPath,
|
|
};
|
|
} else if (typeof opts.subscriptions === 'string') {
|
|
config = { path: opts.subscriptions };
|
|
} else {
|
|
config = { path: this.graphqlPath, ...opts.subscriptions };
|
|
}
|
|
|
|
this.subscriptionsPath = config.path;
|
|
this.subscriptionServer = this.createSubscriptionServer(
|
|
this.http,
|
|
config,
|
|
);
|
|
}
|
|
|
|
if (opts.engineProxy || opts.engineInRequestPath) this.createEngine(opts);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
if (this.engineProxy) {
|
|
this.engineProxy.listen(
|
|
{
|
|
graphqlPaths: [this.graphqlPath],
|
|
port: options.http.port,
|
|
httpServer: this.http,
|
|
launcherOptions: options.engineLauncherOptions,
|
|
},
|
|
() => {
|
|
this.engineProxy.engineListeningAddress.url = require('url').resolve(
|
|
this.engineProxy.engineListeningAddress.url,
|
|
this.graphqlPath,
|
|
);
|
|
resolve(this.engineProxy.engineListeningAddress);
|
|
},
|
|
);
|
|
this.engineProxy.on('error', reject);
|
|
return;
|
|
}
|
|
|
|
// all options for http listeners
|
|
// https://nodejs.org/api/net.html#net_server_listen_options_callback
|
|
// https://github.com/apollographql/apollo-server/pull/979/files/33ea0c92a1e4e76c8915ff08806f15dae391e1f0#discussion_r184470435
|
|
// https://github.com/apollographql/apollo-server/pull/979#discussion_r184471445
|
|
function listenCallback() {
|
|
const listeningAddress: any = this.http.address();
|
|
// Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
|
|
// corresponding loopback ip. Note that the url field we're setting is
|
|
// primarily for consumption by our test suite. If this heuristic is
|
|
// wrong for your use case, explicitly specify a frontend host (in the
|
|
// `frontends.host` field in your engine config, or in the `host`
|
|
// option to ApolloServer.listen).
|
|
let hostForUrl = listeningAddress.address;
|
|
if (
|
|
listeningAddress.address === '' ||
|
|
listeningAddress.address === '::'
|
|
)
|
|
hostForUrl = 'localhost';
|
|
|
|
listeningAddress.url = require('url').format({
|
|
protocol: 'http',
|
|
hostname: hostForUrl,
|
|
port: listeningAddress.port,
|
|
pathname: this.graphqlPath,
|
|
});
|
|
|
|
resolve(listeningAddress);
|
|
}
|
|
|
|
if (options.http.handle) {
|
|
this.http.listen(
|
|
options.http.handle,
|
|
options.http.backlog,
|
|
listenCallback.bind(this),
|
|
);
|
|
} else {
|
|
this.http.listen(options.http, listenCallback.bind(this));
|
|
}
|
|
});
|
|
}
|
|
|
|
public async stop() {
|
|
if (this.engineProxy) await this.engineProxy.stop();
|
|
if (this.subscriptionServer) await this.subscriptionServer.close();
|
|
if (this.http) await new Promise(s => this.http.close(s));
|
|
}
|
|
|
|
private createSubscriptionServer(
|
|
server: HttpServer,
|
|
config: SubscriptionServerOptions,
|
|
) {
|
|
const { onDisconnect, onConnect, keepAlive, path } = config;
|
|
|
|
return SubscriptionServer.create(
|
|
{
|
|
schema: this.schema,
|
|
execute,
|
|
subscribe,
|
|
onConnect: onConnect
|
|
? onConnect
|
|
: (connectionParams: Object) => ({ ...connectionParams }),
|
|
onDisconnect: onDisconnect,
|
|
onOperation: async (_: string, connection: ExecutionParams) => {
|
|
connection.formatResponse = (value: ExecutionResult) => ({
|
|
...value,
|
|
errors:
|
|
value.errors &&
|
|
formatApolloErrors([...value.errors], {
|
|
formatter: this.requestOptions.formatError,
|
|
debug: this.requestOptions.debug,
|
|
logFunction: this.requestOptions.logFunction,
|
|
}),
|
|
});
|
|
let context: Context = this.context ? this.context : { connection };
|
|
|
|
try {
|
|
context =
|
|
typeof this.context === 'function'
|
|
? await this.context({ connection })
|
|
: context;
|
|
} catch (e) {
|
|
throw formatApolloErrors([e], {
|
|
formatter: this.requestOptions.formatError,
|
|
debug: this.requestOptions.debug,
|
|
logFunction: this.requestOptions.logFunction,
|
|
})[0];
|
|
}
|
|
|
|
return { ...connection, context };
|
|
},
|
|
keepAlive,
|
|
},
|
|
{
|
|
server,
|
|
path,
|
|
},
|
|
);
|
|
}
|
|
|
|
private createEngine({ engineInRequestPath, engineProxy }: ListenOptions) {
|
|
// only access this onces as its slower on node
|
|
const { ENGINE_API_KEY, ENGINE_CONFIG } = process.env;
|
|
if (engineProxy === false && (ENGINE_API_KEY || ENGINE_CONFIG)) {
|
|
console.warn(
|
|
'engine is set to false when creating ApolloServer but either ENGINE_CONFIG or ENGINE_API_KEY was found in the environment',
|
|
);
|
|
}
|
|
let ApolloEngine;
|
|
if (engineProxy) {
|
|
// detect engine if it is set to true or has a config, and possibly load it
|
|
try {
|
|
ApolloEngine = require('apollo-engine').ApolloEngine;
|
|
} catch (e) {
|
|
console.warn(`ApolloServer was unable to load Apollo Engine yet engine was configured in the options when creating this ApolloServer? To fix this, run the following command:
|
|
|
|
npm install apollo-engine --save
|
|
`);
|
|
}
|
|
|
|
this.engineProxy = new ApolloEngine(
|
|
typeof engineProxy === 'boolean' ? undefined : engineProxy,
|
|
);
|
|
}
|
|
|
|
// XXX should this allow for header overrides from graphql-playground?
|
|
if (this.engineProxy || engineInRequestPath) this.engineEnabled = true;
|
|
}
|
|
|
|
request(request: Request) {
|
|
let context: Context = this.context ? this.context : { request };
|
|
|
|
//Defer context resolution to inside of runQuery
|
|
context =
|
|
typeof this.context === 'function'
|
|
? () => this.context({ req: request })
|
|
: context;
|
|
|
|
return {
|
|
schema: this.schema,
|
|
tracing: this.engineEnabled,
|
|
cacheControl: this.engineEnabled,
|
|
context,
|
|
// allow overrides from options
|
|
...this.requestOptions,
|
|
};
|
|
}
|
|
}
|