mirror of
https://github.com/vale981/apollo-server
synced 2025-03-05 09:41:40 -05:00
Revert "Add Authentication and File Uploads as guides and feature (#1956)"
This reverts commit 61f2e47108
.
This commit is contained in:
parent
61f2e47108
commit
fb82f1fd5f
4 changed files with 248 additions and 515 deletions
|
@ -26,7 +26,6 @@ sidebar_categories:
|
|||
- features/directives
|
||||
- features/creating-directives
|
||||
- features/test-utils
|
||||
- features/file-uploads
|
||||
# Schema stitching:
|
||||
# - features/schema-stitching
|
||||
# - features/remote-schemas
|
||||
|
@ -38,7 +37,6 @@ sidebar_categories:
|
|||
- deployment/lambda
|
||||
- deployment/now
|
||||
Related Guides:
|
||||
- best-practices/access-control
|
||||
- title: Monitoring
|
||||
href: https://www.apollographql.com/docs/guides/monitoring.html
|
||||
- title: Versioning
|
||||
|
|
|
@ -1,257 +0,0 @@
|
|||
---
|
||||
title: Access Control
|
||||
description: How to authorize users and control permissions in your GraphQL API
|
||||
---
|
||||
|
||||
At some point (probably pretty early on) when building a GraphQL endpoint, you’ll probably have to face the question of how to control who can see and interact with the data in your API.
|
||||
|
||||
**Authentication** is determining whether a user is logged in or not, and subsequently figuring out _which_ user someone is. **Authorization** is then deciding what the user has permission to do or see.
|
||||
|
||||
This article will primarily be focusing on how to set up authorization for your schema once you know about the user trying to make the request, but we’ll go through one example of authentication just to get some _context_ for what we’re doing.
|
||||
|
||||
<h2 id="context">Putting user info on the context</h2>
|
||||
|
||||
Before we get into figuring out user permissions, we have to figure out how to recognize a user first. From HTTP headers, to JSON web tokens, there are a number of ways to handle authentication of users, but once you have your user, controlling access looks pretty similar.
|
||||
|
||||
We’ll be using a login token in an HTTP authorization header as an example.
|
||||
|
||||
```js
|
||||
// using apollo-server 2.x
|
||||
const { ApolloServer } = require('apollo-server');
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
context: ({ req }) => {
|
||||
// get the user token from the headers
|
||||
const token = req.headers.authorization || '';
|
||||
|
||||
// try to retrieve a user with the token
|
||||
const user = getUser(token);
|
||||
|
||||
// add the user to the context
|
||||
return { user };
|
||||
},
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`)
|
||||
});
|
||||
```
|
||||
|
||||
So what’s happening here, exactly? This block of code is setting up a new GraphQL server, using Apollo Server 2.0. This new version of Apollo Server simplifies the API for creating new servers, and has some more intelligent defaults. You can read more about it [here](https://blog.apollographql.com/apollo-server-2-0-30c9bbb4ab5e)!
|
||||
|
||||
In this constructor, we pass type definitions and resolvers to the constructor as well as a function to build our `context` object. The `context` object is one that gets passed to every single resolver at every level, so we can access it anywhere in our schema code. It’s where we can store things like data fetchers, database connections, and (conveniently) information about the user making the request.
|
||||
|
||||
Since the context is generated again with every new request, we don’t have to worry about cleaning up user data at the end of execution.
|
||||
|
||||
The context function here looks at the request headers, pulls off the header named `authorization`, and stores it to a variable. It then calls a `getUser` function with that token, and expects a user to be returned if the token is valid. After that, it returns a context object containing the (potential) user, for all of our resolvers to use.
|
||||
|
||||
The specifics of retrieving a user will look different for each method of authentication, but the final part will look about the same every time. The authorization needs for your schema may require you to put nothing more than `{ loggedIn: true }` into context, but also may require an id or roles, like `{ user: { id: 12345, roles: ['user', 'admin'] } }`.
|
||||
|
||||
In the next section, we’ll look at ways to use the user information we now have to secure your schema.
|
||||
|
||||
<h2 id="schema-auth">Schema authorization</h2>
|
||||
|
||||
Once we have information about the user making a request, the most basic thing we can do is deny them the ability to run a query at all based on their roles. This is an all-or-nothing approach to authorization that we’ll start with because it’s the simplest. If you choose to block users like this, no fields will be publicly queryable.
|
||||
|
||||
We would want to do this only on very restrictive environments where there is no public access to the schema or any fields, like an internal tool or maybe an independent micro service that we don’t want exposed to the public.
|
||||
|
||||
To do this kind of authorization, we can just modify the context function.
|
||||
|
||||
```js
|
||||
context: ({ req }) => {
|
||||
// get the user token from the headers
|
||||
const token = req.headers.authorization || '';
|
||||
|
||||
// try to retrieve a user with the token
|
||||
const user = getUser(token);
|
||||
|
||||
// optionally block the user
|
||||
// we could also check user roles/permissions here
|
||||
if (!user) throw new AuthorizationError('you must be logged in');
|
||||
|
||||
// add the user to the context
|
||||
return { user };
|
||||
},
|
||||
```
|
||||
|
||||
The only difference from the basic context function is the check for the user. If no user exists or if lookup fails, the function throws an error, and none of the query gets executed.
|
||||
|
||||
<h2 id="resolver-auth">Authorization in resolvers</h2>
|
||||
|
||||
Schema authorization may be useful in specific instances, but more commonly, GraphQL schemas will have some fields that need to be public. An example of this would be a news site that wants to show article previews to anyone, but restrict the full body of articles to paying customers only.
|
||||
|
||||
Luckily, GraphQL offers very granular control over data. In GraphQL servers, individual field resolvers have the ability to check user roles and make decisions as to what to return for each user. In the previous sections, we saw how to attach user information to the context object. In the rest of the article, we’ll discuss how to use that context object.
|
||||
|
||||
For our first example, let’s look at a resolver that’s only accessible with a valid user:
|
||||
|
||||
```js
|
||||
users: (root, args, context) => {
|
||||
// In this case, we'll pretend there is no data when
|
||||
// we're not logged in. Another option would be to
|
||||
// throw an error.
|
||||
if (!context.user) return [];
|
||||
|
||||
return ['bob', 'jake'];
|
||||
}
|
||||
```
|
||||
|
||||
This example is a field in our schema named `users` that returns a list of users’ names. The `if` check on the first line of the function looks at the `context` generated from our request, checks for a `user` object, and if one doesn’t exist, returns `null` for the whole field.
|
||||
|
||||
One choice to make when building out our resolvers is what an unauthorized field should return. In some use cases, returning `null` here is perfectly valid. Alternatives to this would be to return an empty array, `[]` or to throw an error, telling the client that they’re not allowed to access that field. For the sake of simplicity, we just returned `[]` in this example.
|
||||
|
||||
Now let’s expand that example a little further, and only allow users with an `admin` role to look at our user list. After all, we probably don’t want just anyone to have access to all our users.
|
||||
|
||||
```js
|
||||
users: (root, args, context) => {
|
||||
if (!context.user || !context.user.roles.includes('admin')) return null;
|
||||
return context.models.User.getAll();
|
||||
}
|
||||
```
|
||||
|
||||
This example looks almost the same as the previous one, with one addition: it expects the `roles` array on a user to include an `admin` role. Otherwise, it returns `null`. The benefit of doing authorization like this is that we can short-circuit our resolvers and not even call lookup functions when we don’t have permission to use them, limiting the possible errors that could expose sensitive data.
|
||||
|
||||
Because our resolvers have access to everything in the context, an important question we need to ask is how much information we want in the context. For example, we don’t need the user’s id, name, or age (at least not yet). It’s best to keep things out of the context until they’re needed, since they’re easy to add back in later.
|
||||
|
||||
<h2 id="models-auth">Authorization in data models</h2>
|
||||
|
||||
As our server gets more complex, there will probably be multiple places in the schema that need to fetch the same kind of data. In our last example, you may have noticed the return array was replaced with a call to `context.models.User.getAll()`.
|
||||
|
||||
Since the very beginning, [we’ve recommended](https://www.apollographql.com/docs/graphql-tools/connectors.html) moving the actual data fetching and transformation logic from resolvers to centralized Model objects that each represent a concept from your application: User, Post, etc. This allows you to make your resolvers a thin routing layer, and put all of your business logic in one place.
|
||||
|
||||
For example, a model file for `User` would include all the logic for operating on users, and may look something like…
|
||||
|
||||
```js
|
||||
export const User = {
|
||||
getAll: () => { /* fetching/transform logic for all users */ },
|
||||
getById: (id) => { /* fetching/transform logic for a single user */ },
|
||||
getByGroupId: (id) => { /* fetching/transform logic for a group of users */ },
|
||||
};
|
||||
```
|
||||
|
||||
In the following example, our schema has multiple ways to request a single user…
|
||||
|
||||
```js
|
||||
type Query {
|
||||
user (id: ID!): User
|
||||
article (id: ID!): Article
|
||||
}
|
||||
|
||||
type Article {
|
||||
author: User
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
name: String!
|
||||
}
|
||||
```
|
||||
|
||||
Rather than having the same fetching logic for a single user in two separate places, it usually makes sense to move that logic to the model file. You may have guessed, with all this talk of model files in an authorization article, that authorization is another great thing to delegate to the model, just like data fetching. You would be right.
|
||||
|
||||
**Delegating authorization to models**
|
||||
|
||||
You may have noticed that our models also exist on the context, alongside the user object we added earlier. We can add the models to the context in exactly the same way as we did the user.
|
||||
|
||||
```js
|
||||
context: ({ req }) => {
|
||||
// get the user token from the headers
|
||||
const token = req.headers.authentication || '';
|
||||
|
||||
// try to retrieve a user with the token
|
||||
const user = getUser(token);
|
||||
|
||||
// optionally block the user
|
||||
// we could also check user roles/permissions here
|
||||
if (!user) throw new AuthorizationError('you must be logged in to query this schema');
|
||||
|
||||
// add the user to the context
|
||||
return {
|
||||
user,
|
||||
models: {
|
||||
User: generateUserModel({ user }),
|
||||
...
|
||||
}
|
||||
};
|
||||
},
|
||||
```
|
||||
|
||||
Starting to generate our models with a function requires a small refactor, that would leave our User model looking something like this:
|
||||
|
||||
```js
|
||||
export const generateUserModel = ({ user }) => ({
|
||||
getAll: () => { /* fetching/transform logic for all users */ },
|
||||
getById: (id) => { /* fetching/transform logic for a single user */ },
|
||||
getByGroupId: (id) => { /* fetching/transform logic for a group of users */ },
|
||||
});
|
||||
```
|
||||
|
||||
Now any model method in `User` has access to the same `user` information that resolvers already had, allowing us to refactor the `getAll` function to do the permissions check directly rather than having to put it in the resolver:
|
||||
|
||||
```js
|
||||
getAll: () => {
|
||||
if(!user || !user.roles.includes('admin') return null;
|
||||
return fetch('http://myurl.com/users');
|
||||
}
|
||||
```
|
||||
|
||||
<h2 id="directives-auth">Authorization via Custom Directives</h2>
|
||||
|
||||
Another way to go about authorization is via GraphQL Schema Directives. A directive is an identifier preceded by a `@` character, optionally followed by a list of named arguments, which can appear after almost any form of syntax in the GraphQL query or schema languages.
|
||||
|
||||
Check out this example of an authorization directive:
|
||||
|
||||
```js
|
||||
const typeDefs = `
|
||||
directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
REVIEWER
|
||||
USER
|
||||
}
|
||||
|
||||
type User @auth(requires: USER) {
|
||||
name: String
|
||||
banned: Boolean @auth(requires: ADMIN)
|
||||
canPost: Boolean @auth(requires: REVIEWER)
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
The `@auth` directive can be called directly on the type, or on the fields if you want to limit access to specific fields as shown in the example above. The logic behind authorization is hidden away in the directive implementation.
|
||||
|
||||
One way of implementing the `@auth` directive is via the [SchemaDirectiveVisitor](https://www.apollographql.com/docs/graphql-tools/schema-directives.html) class from [graphql-tools](https://github.com/apollographql/graphql-tools). Ben Newman covered creating a sample `@deprecated` and `@rest` directive in this [excellent article](https://blog.apollographql.com/reusable-graphql-schema-directives-131fb3a177d1). You can draw inspiration from these examples.
|
||||
|
||||
|
||||
<h2 id="rest-auth">Authorization outside of GraphQL</h2>
|
||||
|
||||
If you’re using a REST API that has built-in authorization, like with an HTTP header, you have one more option. Rather than doing any authentication or authorization work in the GraphQL layer (in resolvers/models), it’s possible to simply pass through the headers or cookies to your REST endpoint and let it do the work.
|
||||
|
||||
Here’s an example:
|
||||
|
||||
```js
|
||||
// src/server.js
|
||||
context: ({ req }) => {
|
||||
// pass the request information through to the model
|
||||
return {
|
||||
user,
|
||||
models: {
|
||||
User: generateUserModel({ req }),
|
||||
...
|
||||
}
|
||||
};
|
||||
},
|
||||
```
|
||||
|
||||
```js
|
||||
// src/models/user.js
|
||||
export const generateUserModel = ({ req }) => ({
|
||||
getAll: () => {
|
||||
return fetch('http://myurl.com/users', { headers: req.headers });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If your REST endpoint is already backed by some form of authorization, this cuts down a lot of the logic that needs to get built in the GraphQL layer. This can be a great option when building a GraphQL API over an existing REST API that has everything you need already built in.
|
248
docs/source/best-practices/authentication.md
Normal file
248
docs/source/best-practices/authentication.md
Normal file
|
@ -0,0 +1,248 @@
|
|||
---
|
||||
title: Auth
|
||||
description: Securing our app and serving our users
|
||||
---
|
||||
|
||||
<h2 id="auth-background">Background: Authentication vs. Authorization</h2>
|
||||
|
||||
**Authentication** describes a process where an application proves the identity of a user, meaning someone claiming to be a certain user through the client is the actual user that has permission to make a request to the server. In most systems, a user and server share a handshake and token that uniquely pairs them together, ensuring both sides know they are communicating with their intended target.
|
||||
|
||||
**Authorization** defines what a user, such as admin or user, is allowed to do. Generally a server will authenticate users and provide them an authorization role that permits the user to perform a subset of all possible operations, such as read and not write.
|
||||
|
||||
<h2>Auth in GraphQL</h2>
|
||||
|
||||
GraphQL offers similar authentication and authorization mechanics as REST and other data fetching solutions with the possibility to control more fine grain access within a single request. There are two common approaches: schema authorization and operation authorization.
|
||||
|
||||
**Schema authorization** follows a similar guidance to REST, where the entire request and response is checked for an authenticated user and authorized to access the servers data.
|
||||
|
||||
**Operation authorization** takes advantage of the flexibility of GraphQL to provide public portions of the schema that don't require any authorization and private portions that require authentication and authorization.
|
||||
|
||||
> Authorization within our GraphQL resolvers is a great first line of defense for securing our application. We recommended having similar authorization patterns within our data fetching models to ensure a user is authorized at every level of data fetching and updating.
|
||||
|
||||
<h2>Authenticating users</h2>
|
||||
|
||||
All of the approaches require that users be authenticated with the server. If our system already has login method setup to authenticate users and provide credentials that can be used in subsequent requests, we can use this same system to authenticate GraphQL requests. With that said, if we are creating a new infrastructure for user authentication, we can follow the existing best practice to authenticate users. For a full example of authentication, follow [this example](#auth-example), which uses [passport.js](http://www.passportjs.org/).
|
||||
|
||||
<h2>Schema Authorization</h2>
|
||||
|
||||
Schema authorization is useful for GraphQL endpoints that require known users and allow access to all fields inside of a GraphQL endpoint. This approach is useful for internal applications, which are used by a group that is known and generally trusted. Additionally it's common to have separate GraphQL services for different features or products that are entirely available to users, meaning if a user is authenticated, they are authorized to access all the data. Since schema authorization does not need to be aware of the GraphQL layer, our server can add a middleware in front of the GraphQL layer to ensure authorization.
|
||||
|
||||
```js
|
||||
// authenticate for schema usage
|
||||
const context = ({ req }) => {
|
||||
const user = myAuthenticationLookupCode(req);
|
||||
if (!user) {
|
||||
throw new Error("You need to be authenticated to access this schema!");
|
||||
}
|
||||
|
||||
return { user }
|
||||
};
|
||||
|
||||
const server = new ApolloServer({ typeDefs, resolvers, context });
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`)
|
||||
});
|
||||
```
|
||||
|
||||
Currently this server will allow any authenticated user to request all fields in the schema, which means that authorization is all or nothing. While some applications provide a shared view of the data to all users, many use cases require scoping authorizations and limiting what some users can see. The authorization scope is shared across all resolvers, so this code adds the user id and scope to the context.
|
||||
|
||||
```js
|
||||
const { ForbiddenError } = require("apollo-server");
|
||||
|
||||
const context = ({ req }) => {
|
||||
const user = myAuthenticationLookupCode(req);
|
||||
if (!user) {
|
||||
throw new ForbiddenError(
|
||||
"You need to be authenticated to access this schema!"
|
||||
);
|
||||
}
|
||||
|
||||
const scope = lookupScopeForUser(user);
|
||||
|
||||
return { user, scope };
|
||||
};
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
context
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
||||
```
|
||||
|
||||
Now within a resolver, we are able to check the user's scope. If the user is not an administrator and `allTodos` are requested, a GraphQL specific forbidden error is thrown. Apollo Server will handle associate the error with the particular path and return it along with any other data successfully requested, such as `myTodos`, to the client.
|
||||
|
||||
```js
|
||||
const { ForbiddenError } = require("apollo-server");
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
allTodos: (source, args, context) => {
|
||||
if (context.scope !== "ADMIN") {
|
||||
throw ForbiddenError("Need Administrator Privileges");
|
||||
}
|
||||
return context.Todos.getAll();
|
||||
},
|
||||
myTodos: (source, args, context) => {
|
||||
return context.Todos.getById(context.user_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The major downside to schema authorization is that all requests must be authenticated, which prevents unauthenticated requests to access information that should be publicly accessible, such as a home page. The next approach, partial query authorization, enables a portion of the schema to be public and authorize portions of the schema to authenticated users.
|
||||
|
||||
## Operation Authorization
|
||||
|
||||
Operation authorization removes the catch all portion of our context function that throws an unauthenticated error, moving the authorization check within resolvers. The instantiation of the server becomes:
|
||||
|
||||
```js
|
||||
const context = ({ req }) => {
|
||||
const user = myAuthenticationLookupCode(req);
|
||||
if (!user) {
|
||||
return { user: null, scope: null }
|
||||
}
|
||||
|
||||
const scope = lookupScopeForUser(user);
|
||||
return { user, scope }
|
||||
};
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
context
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Serverready at ${url}`)
|
||||
});
|
||||
```
|
||||
|
||||
The benefit of doing operation authorization is that private and public data is more easily managed an enforced. Take for example a schema that allows finding `allTodos` in the app (an administrative action), seeing any `publicTodos` which requires no authorization, and returning just a single users todos via `myTodos`. Using Apollo Server, we can easily build complex authorization models like so:
|
||||
|
||||
```js
|
||||
const { ForbiddenError, AuthenticationError } = require("apollo-server");
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
allTodos: (source, args, context) => {
|
||||
if (!context.scope) {
|
||||
throw AuthenticationError("You must be logged in to see all todos");
|
||||
}
|
||||
|
||||
if (context.scope !== "ADMIN") {
|
||||
throw ForbiddenError("You must be an administrator to see all todos");
|
||||
}
|
||||
|
||||
return context.Todos.getAllTodos();
|
||||
},
|
||||
publicTodos: (source, args, context) => {
|
||||
return context.Todos.getPublicTodos();
|
||||
},
|
||||
myTodos: (source, args, context) => {
|
||||
if (!context.scope) {
|
||||
throw AuthenticationError("You must be logged in to see all todos");
|
||||
}
|
||||
|
||||
return context.Todos.getByUserId(context.user.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Should I send a password in a mutation?
|
||||
|
||||
Since GraphQL queries are sent to a server in the same manner as REST requests, the same policies apply to sending sensitive data over the wire. The current best practice is to provide an encrypted connection over https or wss if we are using websockets. Provided we setup this layer, passwords and other sensitive information should be secure.
|
||||
|
||||
## Auth Example
|
||||
|
||||
If you are new setting up new infrastructure or would like to understand an example of how to adapt your existing login system, you can follow this example using passport.js. We will use this example of authentication in the subsequent sections. To skip this section, jump down to the
|
||||
|
||||
```shell
|
||||
npm install --save express passport body-parser express-session node-uuid passport-local apollo-server graphql
|
||||
```
|
||||
|
||||
```js
|
||||
const bodyParser = require('body-parser');
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const session = require('express-session');
|
||||
const uuid = require('node-uuid');
|
||||
```
|
||||
|
||||
After installing and importing the necessary packages, this code checks the user's password and attaches their id to the request.
|
||||
|
||||
```js
|
||||
let LocalStrategy = require('passport-local').Strategy;
|
||||
const { DB } = require('./schema/db.js');
|
||||
|
||||
passport.use(
|
||||
'local',
|
||||
new LocalStrategy(function(username, password, done) {
|
||||
let checkPassword = DB.Users.checkPassword(username, password);
|
||||
let getUser = checkPassword
|
||||
.then(is_login_valid => {
|
||||
if (is_login_valid) return DB.Users.getUserByUsername(username);
|
||||
else throw new Error('invalid username or password');
|
||||
})
|
||||
.then(user => done(null, user))
|
||||
.catch(err => done(err));
|
||||
}),
|
||||
);
|
||||
|
||||
passport.serializeUser((user, done) => done(null, user.id));
|
||||
|
||||
passport.deserializeUser((id, done) =>
|
||||
DB.Users.get(id).then((user, err) => done(err, user))
|
||||
);
|
||||
```
|
||||
|
||||
Now that passport has been setup, we initialize the server application to use the passport middleware, attaching the user id to the request.
|
||||
|
||||
```js
|
||||
const app = express();
|
||||
|
||||
//passport's session piggy-backs on express-session
|
||||
app.use(
|
||||
session({
|
||||
genid: function(req) {
|
||||
return uuid.v4();
|
||||
},
|
||||
secret: 'Z3]GJW!?9uP"/Kpe',
|
||||
})
|
||||
);
|
||||
|
||||
//Provide authentication and user information to all routes
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
```
|
||||
|
||||
Finally we provide the login route and start Apollo Server.
|
||||
|
||||
```js
|
||||
const { typeDefs, resolvers } = require('./schema');
|
||||
|
||||
//login route for passport
|
||||
app.use('/login', bodyParser.urlencoded({ extended: true }));
|
||||
app.post(
|
||||
'/login',
|
||||
passport.authenticate('local', {
|
||||
successRedirect: '/',
|
||||
failureRedirect: '/login',
|
||||
failureFlash: true,
|
||||
}),
|
||||
);
|
||||
|
||||
//Depending on the authorization model chosen, you may include some extra middleware here before you instantiate the server
|
||||
|
||||
//Create and start your apollo server
|
||||
const server = new ApolloServer({ typeDefs, resolvers, app });
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`)
|
||||
});
|
||||
```
|
|
@ -1,256 +0,0 @@
|
|||
---
|
||||
title: File uploads
|
||||
description: Implementing file uploads in GraphQL apps
|
||||
---
|
||||
|
||||
File uploads are a requirement for many applications. Apollo Server supports the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) for uploading files as mutation arguments using [apollo-upload-server](https://github.com/jaydenseric/apollo-upload-server).
|
||||
|
||||
## File upload with default options
|
||||
|
||||
Apollo Server automatically adds the `Upload` scalar to the schema when you are not setting the schema manually.
|
||||
|
||||
```js
|
||||
const { ApolloServer, gql } = require('apollo-server');
|
||||
|
||||
const typeDefs = gql`
|
||||
type File {
|
||||
filename: String!
|
||||
mimetype: String!
|
||||
encoding: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
uploads: [File]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
singleUpload(file: Upload!): File!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
files: () => {
|
||||
// Return the record of files uploaded from your DB or API or filesystem.
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
async singleUpload(parent, { file }) {
|
||||
const { stream, filename, mimetype, encoding } = await file;
|
||||
|
||||
// 1. Validate file metadata.
|
||||
|
||||
// 2. Stream file contents into local filesystem or cloud storage:
|
||||
// https://nodejs.org/api/stream.html
|
||||
|
||||
// 3. Record the file upload in your DB.
|
||||
// const id = await recordFile( … )
|
||||
|
||||
return { stream, filename, mimetype, encoding };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
||||
```
|
||||
|
||||
## File upload with schema param
|
||||
|
||||
In a situation where a schema is set manually using `makeExecutableSchema` and passed to the `ApolloServer` constructor using the schema params, add the `Upload` scalar to the type definitions and `Upload` to the resolver as shown in the example below:
|
||||
|
||||
```js
|
||||
const { ApolloServer, makeExecutableSchema, gql, GraphQLUpload } = require('apollo-server');
|
||||
|
||||
const typeDefs = gql`
|
||||
scalar Upload
|
||||
type File {
|
||||
filename: String!
|
||||
mimetype: String!
|
||||
encoding: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
uploads: [File]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
singleUpload(file: Upload!): File!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Upload: GraphQLUpload,
|
||||
Query: {
|
||||
files: () => {
|
||||
// Return the record of files uploaded from your DB or API or filesystem.
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
async singleUpload(parent, { file }) {
|
||||
const { stream, filename, mimetype, encoding } = await file;
|
||||
|
||||
// 1. Validate file metadata.
|
||||
|
||||
// 2. Stream file contents into local filesystem or cloud storage:
|
||||
// https://nodejs.org/api/stream.html
|
||||
|
||||
// 3. Record the file upload in your DB.
|
||||
// const id = await recordFile( … )
|
||||
|
||||
return { stream, filename, mimetype, encoding };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Scalar Upload
|
||||
|
||||
The `Upload` type automatically added to the schema by Apollo Server resolves an object containing the following:
|
||||
|
||||
- `stream`
|
||||
- `filename`
|
||||
- `mimetype`
|
||||
- `encoding`
|
||||
|
||||
|
||||
### File upload options
|
||||
|
||||
The `ApolloServer` constructor supports the following configuration properties. They are:
|
||||
|
||||
- `maxFieldSize`: represents allowed non-file multipart form field size in bytes.
|
||||
- `maxFileSize`: represents the allowed file size in bytes.
|
||||
- `maxFiles`: represents the allowed number of files. It can accept as many files as possible.
|
||||
|
||||
|
||||
## Client setup
|
||||
|
||||
From the client side, you need to install the `apollo-upload-client` package. It enables file uploads via GraphQL mutations.
|
||||
|
||||
```sh
|
||||
npm install apollo-upload-client
|
||||
```
|
||||
|
||||
You will then need to initialize your [Apollo Client](https://apollographql.com/docs/link#apollo-client) instance with a terminating [Apollo Link](https://apollographql.com/docs/link), created by calling [`createUploadlink`](https://github.com/jaydenseric/apollo-upload-client#function-createuploadlink). For example:
|
||||
|
||||
```js
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { createUploadLink } from 'apollo-upload-client';
|
||||
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: createUploadLink(),
|
||||
});
|
||||
```
|
||||
|
||||
> Note: [Apollo Boost](https://www.apollographql.com/docs/react/essentials/get-started.html#apollo-boost) does not support Apollo Link overrides, so if you're using Apollo Boost and want to use `apollo-upload-client`, you will need to switch to the full version of Apollo Client. See the [Apollo Boost migration](https://www.apollographql.com/docs/react/advanced/boost-migration.html) docs for help migrating from Apollo Boost to Apollo Client.
|
||||
|
||||
_File uploads example from the client for a single file:_
|
||||
|
||||
```js
|
||||
import gql from 'graphql-tag';
|
||||
import { Mutation } from 'react-apollo';
|
||||
|
||||
export const UPLOAD_FILE = gql`
|
||||
mutation uploadFile($file: Upload!) {
|
||||
uploadFile(file: $file) {
|
||||
filename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const uploadFile = () => {
|
||||
return (
|
||||
<Mutation mutation={UPLOAD_FILE}>
|
||||
{uploadFile => (
|
||||
<input
|
||||
type="file"
|
||||
required
|
||||
onChange={({ target: { validity, files: [file] } }) =>
|
||||
validity.valid && uploadFile({ variables: { file } });
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Mutation>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
_File uploads example from the client for multiple files:_
|
||||
|
||||
```js
|
||||
import gql from 'graphql-tag';
|
||||
import { Mutation } from 'react-apollo';
|
||||
|
||||
export const UPLOAD_MULTIPLE_FILES = gql`
|
||||
mutation uploadMultipleFiles($files: [Upload!]!) {
|
||||
uploadMultipleFiles(files: $files) {
|
||||
filename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const uploadMultipleFiles = () => {
|
||||
return (
|
||||
<Mutation mutation={UPLOAD_MULTIPLE_FILES}>
|
||||
{uploadFile => (
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
required
|
||||
onChange={({ target: { validity, files } }) =>
|
||||
validity.valid && uploadMultipleFiles({ variables: { files } });
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Mutation>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
_Blob example from the client:_
|
||||
|
||||
```js
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// Apollo Client instance
|
||||
import client from './apollo'
|
||||
|
||||
const file = new Blob(['Foo.'], { type: 'text/plain' })
|
||||
|
||||
// Optional, defaults to `blob`
|
||||
file.name = 'bar.txt'
|
||||
|
||||
client.mutate({
|
||||
mutation: gql`
|
||||
mutation($file: Upload!) {
|
||||
uploadFile(file: $file) {
|
||||
filename
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { file }
|
||||
})
|
||||
```
|
||||
|
||||
Use [FileList](https://developer.mozilla.org/en/docs/Web/API/FileList), [File](https://developer.mozilla.org/en/docs/Web/API/File), [Blob](https://developer.mozilla.org/en/docs/Web/API/Blob) instances anywhere within query or mutation input variables to send a GraphQL multipart request.
|
||||
|
||||
**Jayden Seric**, author of [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) has [an example app on GitHub](https://github.com/jaydenseric/apollo-upload-examples/tree/master/app). It's a web app using [Next.js](https://github.com/zeit/next.js/), [react-apollo](https://github.com/apollographql/react-apollo), and [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client).
|
Loading…
Add table
Reference in a new issue