Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apollo Server 2: Add file uploads #1071

Merged
merged 9 commits into from
May 29, 2018
23 changes: 20 additions & 3 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import {
makeExecutableSchema,
addMockFunctionsToSchema,
IResolvers,
mergeSchemas,
} from 'graphql-tools';
import { Server as HttpServer } from 'http';
import {
execute,
Expand Down Expand Up @@ -49,10 +54,10 @@ 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 requestOptions: Partial<GraphQLOptions<any>>;
private graphqlPath: string = '/graphql';
private engineProxy: ApolloEngine;
private engineEnabled: boolean = false;
Expand Down Expand Up @@ -104,7 +109,7 @@ export class ApolloServerBase<Request = RequestInit> {
: makeExecutableSchema({
typeDefs: Array.isArray(typeDefs) ? typeDefs.join('\n') : typeDefs,
schemaDirectives,
resolvers,
resolvers: resolvers,
});

if (mocks) {
Expand All @@ -123,6 +128,18 @@ export class ApolloServerBase<Request = RequestInit> {
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();

Expand Down
5 changes: 4 additions & 1 deletion packages/apollo-server-express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"accepts": "^1.3.5",
"apollo-server-core": "2.0.0-beta.1",
"apollo-server-module-graphiql": "^1.3.4",
"apollo-upload-server": "^5.0.0",
"body-parser": "^1.18.3",
"cors": "^2.8.4",
"graphql-playground-middleware-express": "^1.6.2"
Expand All @@ -45,7 +46,9 @@
"connect": "3.6.6",
"connect-query": "1.0.0",
"express": "4.16.3",
"multer": "1.3.0"
"form-data": "^2.3.2",
"multer": "1.3.0",
"node-fetch": "^2.1.2"
},
"typings": "dist/index.d.ts",
"typescript": {
Expand Down
79 changes: 79 additions & 0 deletions packages/apollo-server-express/src/ApolloServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import 'mocha';
import * as express from 'express';

import * as request from 'request';
import * as FormData from 'form-data';
import * as fs from 'fs';
import * as fetch from 'node-fetch';
import { createApolloFetch } from 'apollo-fetch';

import { ApolloServerBase } from 'apollo-server-core';
Expand Down Expand Up @@ -253,5 +256,81 @@ describe('apollo-server-express', () => {
});
});
});
describe('file uploads', () => {
it('enabled uploads', async () => {
server = new ApolloServer({
typeDefs: gql`
scalar Upload
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needing to add this line is frustrating. When it is removed, the makeExecutableSchema in the constructor fails. We could delay the evaluation of the typeDefs and resolvers until later when this.schema is used with a getter. This feels a bit weird, since the mocks are added in the constructor, so could or could not affect the typeDefs later added to the schema with enhanceSchema

Before the current and past two commits, we had the upload configuration code in the ApolloServer constructor. However fileuploads seem to be a integration specific concern, so I'm pretty convinced that uploads should be configured in registerServer


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

type Query {
uploads: [File]
}

type Mutation {
singleUpload(file: Upload!): File!
}
`,
resolvers: {
Query: {
uploads: (parent, args) => {},
},
Mutation: {
singleUpload: async (parent, args) => {
expect((await args.file).stream).to.exist;
return args.file;
},
},
},
});
app = express();
registerServer({
app,
server,
});

const { port } = await server.listen({});

const body = new FormData();

body.append(
'operations',
JSON.stringify({
query: gql`
mutation($file: Upload!) {
singleUpload(file: $file) {
filename
encoding
mimetype
}
}
`,
variables: {
file: null,
},
}),
);

body.append('map', JSON.stringify({ 1: ['variables.file'] }));
body.append('1', fs.createReadStream('package.json'));

const resolved = await fetch(`http://localhost:${port}/graphql`, {
method: 'POST',
body,
});
const response = await resolved.json();

expect(response.data.singleUpload).to.deep.equal({
filename: 'package.json',
encoding: '7bit',
mimetype: 'application/json',
});
});
});
});
});
58 changes: 57 additions & 1 deletion packages/apollo-server-express/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import * as corsMiddleware from 'cors';
import { json, OptionsJson } from 'body-parser';
import { createServer, Server as HttpServer } from 'http';
import gui from 'graphql-playground-middleware-express';
import { ApolloServerBase } from 'apollo-server-core';
import { ApolloServerBase, formatApolloErrors } from 'apollo-server-core';
import * as accepts from 'accepts';

import { graphqlExpress } from './expressApollo';

import {
processRequest as processFileUploads,
GraphQLUpload,
} from 'apollo-upload-server';

const gql = String.raw;

export interface ServerRegistration {
app: express.Application;
server: ApolloServerBase<express.Request>;
Expand All @@ -16,8 +23,40 @@ export interface ServerRegistration {
bodyParserConfig?: OptionsJson;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
//https://github.com/jaydenseric/apollo-upload-server#options
uploads?: boolean | Record<string, any>;
}

const fileUploadMiddleware = (
uploadsConfig: Record<string, any>,
server: ApolloServerBase<express.Request>,
) => (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
if (req.is('multipart/form-data')) {
processFileUploads(req, uploadsConfig)
.then(body => {
req.body = body;
next();
})
.catch(error => {
if (error.status && error.expose) res.status(error.status);

next(
formatApolloErrors([error], {
formatter: server.requestOptions.formatError,
debug: server.requestOptions.debug,
logFunction: server.requestOptions.logFunction,
}),
);
});
} else {
next();
}
};

export const registerServer = async ({
app,
server,
Expand All @@ -26,6 +65,7 @@ export const registerServer = async ({
bodyParserConfig,
disableHealthCheck,
onHealthCheck,
uploads,
}: ServerRegistration) => {
if (!path) path = '/graphql';

Expand All @@ -49,6 +89,21 @@ export const registerServer = async ({
});
}

let uploadsMiddleware;
if (uploads !== false) {
server.enhanceSchema({
typeDefs: gql`
scalar Upload
`,
resolvers: { Upload: GraphQLUpload },
});

uploadsMiddleware = fileUploadMiddleware(
typeof uploads !== 'boolean' ? uploads : {},
server,
);
}

// XXX multiple paths?
server.use({
path,
Expand All @@ -59,6 +114,7 @@ export const registerServer = async ({
path,
corsMiddleware(cors),
json(bodyParserConfig),
uploadsMiddleware,
(req, res, next) => {
// make sure we check to see if graphql gui should be on
if (!server.disableTools && req.method === 'GET') {
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-server-hapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"accept": "^3.0.2",
"apollo-server-core": "2.0.0-beta.1",
"apollo-server-module-graphiql": "^1.3.4",
"apollo-upload-server": "^5.0.0",
"boom": "^7.1.0",
"graphql-playground-html": "^1.5.6"
},
Expand Down
38 changes: 37 additions & 1 deletion packages/apollo-server-hapi/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { createServer, Server as HttpServer } from 'http';
import { ApolloServerBase, EngineLauncherOptions } from 'apollo-server-core';
import { parseAll } from 'accept';
import { renderPlaygroundPage } from 'graphql-playground-html';
import {
processRequest as processFileUploads,
GraphQLUpload,
} from 'apollo-upload-server';

import { graphqlHapi } from './hapiApollo';

const gql = String.raw;

export interface ServerRegistration {
app?: hapi.Server;
//The options type should exclude port
Expand All @@ -15,6 +21,7 @@ export interface ServerRegistration {
cors?: boolean;
onHealthCheck?: (req: hapi.Request) => Promise<any>;
disableHealthCheck?: boolean;
uploads?: boolean | Record<string, any>;
}

export interface HapiListenOptions {
Expand All @@ -26,6 +33,18 @@ export interface HapiListenOptions {
launcherOptions?: EngineLauncherOptions;
}

const handleFileUploads = (
uploadsConfig: Record<string, any>,
server: ApolloServerBase<hapi.Request>,
) => async (req: hapi.Request, h: hapi.ResponseToolkit) => {
if (req.mime === 'multipart/form-data') {
Object.defineProperty(req, 'payload', {
value: await processFileUploads(req, uploadsConfig),
writable: false,
});
}
};

export const registerServer = async ({
app,
options,
Expand All @@ -34,6 +53,7 @@ export const registerServer = async ({
path,
disableHealthCheck,
onHealthCheck,
uploads,
}: ServerRegistration) => {
if (!path) path = '/graphql';

Expand Down Expand Up @@ -63,13 +83,29 @@ server.listen({ http: { port: YOUR_PORT_HERE } });
hapiApp = new hapi.Server({ autoListen: false });
}

if (uploads !== false) {
server.enhanceSchema({
typeDefs: gql`
scalar Upload
`,
resolvers: { Upload: GraphQLUpload },
});
}

await hapiApp.ext({
type: 'onRequest',
method: function(request, h) {
method: async function(request, h) {
if (request.path !== path) {
return h.continue;
}

if (uploads !== false) {
await handleFileUploads(
typeof uploads !== 'boolean' ? uploads : {},
server,
)(request, h);
}

if (!server.disableTools && request.method === 'get') {
//perform more expensive content-type check only if necessary
const accept = parseAll(request.headers);
Expand Down