Skip to content

Commit 6721362

Browse files
committed
release: 2.0.0-beta.5
1 parent 95efb6c commit 6721362

18 files changed

+2460
-4
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Changelog and release notes
22

3+
<!-- ## Unreleased -->
4+
35
<!-- Here goes all the unreleased changes descriptions -->
46

5-
## Unreleased
7+
## v2.0.0-beta.5
68

79
### Fixes
810

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "type-graphql",
3-
"version": "2.0.0-beta.4",
3+
"version": "2.0.0-beta.5",
44
"private": false,
55
"description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!",
66
"keywords": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
title: Authorization
3+
id: version-2.0.0-beta.5-authorization
4+
original_id: authorization
5+
---
6+
7+
Authorization is a core feature used in almost all APIs. Sometimes we want to restrict data access or actions for a specific group of users.
8+
9+
In express.js (and other Node.js frameworks) we use middleware for this, like `passport.js` or the custom ones. However, in GraphQL's resolver architecture we don't have middleware so we have to imperatively call the auth checking function and manually pass context data to each resolver, which might be a bit tedious.
10+
11+
That's why authorization is a first-class feature in `TypeGraphQL`!
12+
13+
## How to use
14+
15+
First, we need to use the `@Authorized` decorator as a guard on a field, query or mutation.
16+
Example object type field guards:
17+
18+
```ts
19+
@ObjectType()
20+
class MyObject {
21+
@Field()
22+
publicField: string;
23+
24+
@Authorized()
25+
@Field()
26+
authorizedField: string;
27+
28+
@Authorized("ADMIN")
29+
@Field()
30+
adminField: string;
31+
32+
@Authorized(["ADMIN", "MODERATOR"])
33+
@Field({ nullable: true })
34+
hiddenField?: string;
35+
}
36+
```
37+
38+
We can leave the `@Authorized` decorator brackets empty or we can specify the role/roles that the user needs to possess in order to get access to the field, query or mutation.
39+
By default the roles are of type `string` but they can easily be changed as the decorator is generic - `@Authorized<number>(1, 7, 22)`.
40+
41+
Thus, authorized users (regardless of their roles) can only read the `publicField` or the `authorizedField` from the `MyObject` object. They will receive `null` when accessing the `hiddenField` field and will receive an error (that will propagate through the whole query tree looking for a nullable field) for the `adminField` when they don't satisfy the role constraints.
42+
43+
Sample query and mutation guards:
44+
45+
```ts
46+
@Resolver()
47+
class MyResolver {
48+
@Query()
49+
publicQuery(): MyObject {
50+
return {
51+
publicField: "Some public data",
52+
authorizedField: "Data for logged users only",
53+
adminField: "Top secret info for admin",
54+
};
55+
}
56+
57+
@Authorized()
58+
@Query()
59+
authedQuery(): string {
60+
return "Authorized users only!";
61+
}
62+
63+
@Authorized("ADMIN", "MODERATOR")
64+
@Mutation()
65+
adminMutation(): string {
66+
return "You are an admin/moderator, you can safely drop the database ;)";
67+
}
68+
}
69+
```
70+
71+
Authorized users (regardless of their roles) will be able to read data from the `publicQuery` and the `authedQuery` queries, but will receive an error when trying to perform the `adminMutation` when their roles don't include `ADMIN` or `MODERATOR`.
72+
73+
Next, we need to create our auth checker function. Its implementation may depend on our business logic:
74+
75+
```ts
76+
export const customAuthChecker: AuthChecker<ContextType> = (
77+
{ root, args, context, info },
78+
roles,
79+
) => {
80+
// Read user from context
81+
// and check the user's permission against the `roles` argument
82+
// that comes from the '@Authorized' decorator, eg. ["ADMIN", "MODERATOR"]
83+
84+
return true; // or 'false' if access is denied
85+
};
86+
```
87+
88+
The second argument of the `AuthChecker` generic type is `RoleType` - used together with the `@Authorized` decorator generic type.
89+
90+
Auth checker can be also defined as a class - this way we can leverage the dependency injection mechanism:
91+
92+
```ts
93+
export class CustomAuthChecker implements AuthCheckerInterface<ContextType> {
94+
constructor(
95+
// Dependency injection
96+
private readonly userRepository: Repository<User>,
97+
) {}
98+
99+
check({ root, args, context, info }: ResolverData<ContextType>, roles: string[]) {
100+
const userId = getUserIdFromToken(context.token);
101+
// Use injected service
102+
const user = this.userRepository.getById(userId);
103+
104+
// Custom logic, e.g.:
105+
return user % 2 === 0;
106+
}
107+
}
108+
```
109+
110+
The last step is to register the function or class while building the schema:
111+
112+
```ts
113+
import { customAuthChecker } from "../auth/custom-auth-checker.ts";
114+
115+
const schema = await buildSchema({
116+
resolvers: [MyResolver],
117+
// Register the auth checking function
118+
// or defining it inline
119+
authChecker: customAuthChecker,
120+
});
121+
```
122+
123+
And it's done! 😉
124+
125+
If we need silent auth guards and don't want to return authorization errors to users, we can set the `authMode` property of the `buildSchema` config object to `"null"`:
126+
127+
```ts
128+
const schema = await buildSchema({
129+
resolvers: ["./**/*.resolver.ts"],
130+
authChecker: customAuthChecker,
131+
authMode: "null",
132+
});
133+
```
134+
135+
It will then return `null` instead of throwing an authorization error.
136+
137+
## Recipes
138+
139+
We can also use `TypeGraphQL` with JWT authentication.
140+
Here's an example using `@apollo/server`:
141+
142+
```ts
143+
import { ApolloServer } from "@apollo/server";
144+
import { expressMiddleware } from "@apollo/server/express4";
145+
import express from "express";
146+
import jwt from "express-jwt";
147+
import bodyParser from "body-parser";
148+
import { schema } from "./graphql/schema";
149+
import { User } from "./User.type";
150+
151+
// GraphQL path
152+
const GRAPHQL_PATH = "/graphql";
153+
154+
// GraphQL context
155+
type Context = {
156+
user?: User;
157+
};
158+
159+
// Express
160+
const app = express();
161+
162+
// Apollo server
163+
const server = new ApolloServer<Context>({ schema });
164+
await server.start();
165+
166+
// Mount a JWT or other authentication middleware that is run before the GraphQL execution
167+
app.use(
168+
GRAPHQL_PATH,
169+
jwt({
170+
secret: "TypeGraphQL",
171+
credentialsRequired: false,
172+
}),
173+
);
174+
175+
// Apply GraphQL server middleware
176+
app.use(
177+
GRAPHQL_PATH,
178+
bodyParser.json(),
179+
expressMiddleware(server, {
180+
// Build context
181+
// 'req.user' comes from 'express-jwt'
182+
context: async ({ req }) => ({ user: req.user }),
183+
}),
184+
);
185+
186+
// Start server
187+
await new Promise<void>(resolve => app.listen({ port: 4000 }, resolve));
188+
console.log(`GraphQL server ready at http://localhost:4000/${GRAPHQL_PATH}`);
189+
```
190+
191+
Then we can use standard, token based authorization in the HTTP header like in classic REST APIs and take advantage of the `TypeGraphQL` authorization mechanism.
192+
193+
## Example
194+
195+
See how this works in the [simple real life example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.5/examples/authorization).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
title: Query complexity
3+
id: version-2.0.0-beta.5-complexity
4+
original_id: complexity
5+
---
6+
7+
A single GraphQL query can potentially generate a huge workload for a server, like thousands of database operations which can be used to cause DDoS attacks. In order to limit and keep track of what each GraphQL operation can do, `TypeGraphQL` provides the option of integrating with Query Complexity tools like [graphql-query-complexity](https://github.com/ivome/graphql-query-complexity).
8+
9+
This cost analysis-based solution is very promising, since we can define a “cost” per field and then analyze the AST to estimate the total cost of the GraphQL query. Of course all the analysis is handled by `graphql-query-complexity`.
10+
11+
All we must do is define our complexity cost for the fields, mutations or subscriptions in `TypeGraphQL` and implement `graphql-query-complexity` in whatever GraphQL server that is being used.
12+
13+
## How to use
14+
15+
First, we need to pass `complexity` as an option to the decorator on a field, query or mutation.
16+
17+
Example of complexity
18+
19+
```ts
20+
@ObjectType()
21+
class MyObject {
22+
@Field({ complexity: 2 })
23+
publicField: string;
24+
25+
@Field({ complexity: ({ args, childComplexity }) => childComplexity + 1 })
26+
complexField: string;
27+
}
28+
```
29+
30+
The `complexity` option may be omitted if the complexity value is 1.
31+
Complexity can be passed as an option to any `@Field`, `@FieldResolver`, `@Mutation` or `@Subscription` decorator. If both `@FieldResolver` and `@Field` decorators of the same property have complexity defined, then the complexity passed to the field resolver decorator takes precedence.
32+
33+
In the next step, we will integrate `graphql-query-complexity` with the server that expose our GraphQL schema over HTTP.
34+
You can use it with `express-graphql` like [in the lib examples](https://github.com/slicknode/graphql-query-complexity/blob/b6a000c0984f7391f3b4e886e3df6a7ed1093b07/README.md#usage-with-express-graphql), however we will use Apollo Server like in our other examples:
35+
36+
```ts
37+
async function bootstrap() {
38+
// ... Build GraphQL schema
39+
40+
// Create GraphQL server
41+
const server = new ApolloServer({
42+
schema,
43+
// Create a plugin to allow query complexity calculation for every request
44+
plugins: [
45+
{
46+
requestDidStart: async () => ({
47+
async didResolveOperation({ request, document }) {
48+
/**
49+
* Provides GraphQL query analysis to be able to react on complex queries to the GraphQL server
50+
* It can be used to protect the GraphQL server against resource exhaustion and DoS attacks
51+
* More documentation can be found at https://github.com/ivome/graphql-query-complexity
52+
*/
53+
const complexity = getComplexity({
54+
// GraphQL schema
55+
schema,
56+
// To calculate query complexity properly,
57+
// check only the requested operation
58+
// not the whole document that may contains multiple operations
59+
operationName: request.operationName,
60+
// GraphQL query document
61+
query: document,
62+
// GraphQL query variables
63+
variables: request.variables,
64+
// Add any number of estimators. The estimators are invoked in order, the first
65+
// numeric value that is being returned by an estimator is used as the field complexity
66+
// If no estimator returns a value, an exception is raised
67+
estimators: [
68+
// Using fieldExtensionsEstimator is mandatory to make it work with type-graphql
69+
fieldExtensionsEstimator(),
70+
// Add more estimators here...
71+
// This will assign each field a complexity of 1
72+
// if no other estimator returned a value
73+
simpleEstimator({ defaultComplexity: 1 }),
74+
],
75+
});
76+
77+
// React to the calculated complexity,
78+
// like compare it with max and throw error when the threshold is reached
79+
if (complexity > MAX_COMPLEXITY) {
80+
throw new Error(
81+
`Sorry, too complicated query! ${complexity} exceeded the maximum allowed complexity of ${MAX_COMPLEXITY}`,
82+
);
83+
}
84+
console.log("Used query complexity points:", complexity);
85+
},
86+
}),
87+
},
88+
],
89+
});
90+
91+
// Start server
92+
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
93+
console.log(`GraphQL server ready at ${url}`);
94+
}
95+
```
96+
97+
And it's done! 😉
98+
99+
For more info about how query complexity is computed, please visit [graphql-query-complexity](https://github.com/ivome/graphql-query-complexity).
100+
101+
## Example
102+
103+
See how this works in the [simple query complexity example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.5/examples/query-complexity).

0 commit comments

Comments
 (0)