Skip to content

Commit f50d1d0

Browse files
committed
release: 2.0.0-beta.3
1 parent d1e3700 commit f50d1d0

34 files changed

+3945
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Changelog and release notes
22

3-
## Unreleased
3+
<!-- ## Unreleased -->
44

55
<!-- Here goes all the unreleased changes descriptions -->
66

7+
## v2.0.0-beta.3
8+
79
### Features
810

911
- **Breaking Change**: update `graphql-js` peer dependency to `^16.7.1`

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "type-graphql",
3-
"version": "2.0.0-beta.2",
3+
"version": "2.0.0-beta.3",
44
"private": false,
55
"description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!",
66
"keywords": [
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
title: Authorization
3+
id: version-2.0.0-beta.3-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.3/examples/authorization).
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: Bootstrapping
3+
id: version-2.0.0-beta.3-bootstrap
4+
original_id: bootstrap
5+
---
6+
7+
After creating our resolvers, type classes, and other business-related code, we need to make our app run. First we have to build the schema, then we can expose it with an HTTP server, WebSockets or even MQTT.
8+
9+
## Create Executable Schema
10+
11+
To create an executable schema from type and resolver definitions, we need to use the `buildSchema` function.
12+
It takes a configuration object as a parameter and returns a promise of a `GraphQLSchema` object.
13+
14+
In the configuration object we must provide a `resolvers` property, which is supposed to be an array of resolver classes:
15+
16+
```ts
17+
import { FirstResolver, SecondResolver } from "./resolvers";
18+
// ...
19+
const schema = await buildSchema({
20+
resolvers: [FirstResolver, SecondResolver],
21+
});
22+
```
23+
24+
Be aware that only operations (queries, mutation, etc.) defined in the resolvers classes (and types directly connected to them) will be emitted in schema.
25+
26+
So if we have defined some object types (that implements an interface type [with disabled auto registering](./interfaces.md#registering-in-schema)) but are not directly used in other types definition (like a part of an union, a type of a field or a return type of an operation), we need to provide them manually in `orphanedTypes` options of `buildSchema`:
27+
28+
```ts
29+
import { FirstResolver, SecondResolver } from "../app/src/resolvers";
30+
import { FirstObject } from "../app/src/types";
31+
// ...
32+
const schema = await buildSchema({
33+
resolvers: [FirstResolver, SecondResolver],
34+
// Provide all the types that are missing in schema
35+
orphanedTypes: [FirstObject],
36+
});
37+
```
38+
39+
In case of defining the resolvers array somewhere else (not inline in the `buildSchema`), we need to use the `as const` syntax to inform the TS compiler and satisfy the `NonEmptyArray<T>` constraints:
40+
41+
```ts
42+
// resolvers.ts
43+
export const resolvers = [FirstResolver, SecondResolver] as const;
44+
45+
// schema.ts
46+
import { resolvers } from "./resolvers";
47+
48+
const schema = await buildSchema({ resolvers });
49+
```
50+
51+
There are also other options related to advanced features like [authorization](./authorization.md) or [validation](./validation.md) - you can read about them in docs.
52+
53+
To make `await` work, we need to declare it as an async function. Example of `main.ts` file:
54+
55+
```ts
56+
import { buildSchema } from "type-graphql";
57+
58+
async function bootstrap() {
59+
const schema = await buildSchema({
60+
resolvers: [
61+
// ... Resolvers classes
62+
],
63+
});
64+
65+
// ...
66+
}
67+
68+
bootstrap(); // Actually run the async function
69+
```
70+
71+
## Create an HTTP GraphQL endpoint
72+
73+
In most cases, the GraphQL app is served by an HTTP server. After building the schema we can create the GraphQL endpoint with a variety of tools such as [`graphql-yoga`](https://github.com/dotansimha/graphql-yoga) or [`@apollo/server`](https://github.com/apollographql/apollo-server).
74+
75+
Below is an example using [`@apollo/server`](https://github.com/apollographql/apollo-server):
76+
77+
```ts
78+
import { ApolloServer } from "@apollo/server";
79+
import { startStandaloneServer } from "@apollo/server/standalone";
80+
81+
const PORT = process.env.PORT || 4000;
82+
83+
async function bootstrap() {
84+
// ... Build GraphQL schema
85+
86+
// Create GraphQL server
87+
const server = new ApolloServer({ schema });
88+
89+
// Start server
90+
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
91+
console.log(`GraphQL server ready at ${url}`);
92+
}
93+
94+
bootstrap();
95+
```
96+
97+
Remember to install the `@apollo/server` package from npm - it's not bundled with TypeGraphQL.
98+
99+
Of course you can use the `express-graphql` middleware, `graphql-yoga` or whatever you want 😉
100+
101+
## Create typeDefs and resolvers map
102+
103+
TypeGraphQL provides a second way to generate the GraphQL schema - the `buildTypeDefsAndResolvers` function.
104+
105+
It accepts the same `BuildSchemaOptions` as the `buildSchema` function but instead of an executable `GraphQLSchema`, it creates a typeDefs and resolversMap pair that you can use e.g. with [@graphql-tools/\*`](https://the-guild.dev/graphql/tools):
106+
107+
```ts
108+
import { makeExecutableSchema } from "@graphql-tools/schema";
109+
110+
const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({
111+
resolvers: [FirstResolver, SecondResolver],
112+
});
113+
114+
const schema = makeExecutableSchema({ typeDefs, resolvers });
115+
```
116+
117+
Or even with other libraries that expect the schema info in that shape, like [`apollo-link-state`](https://github.com/apollographql/apollo-link-state):
118+
119+
```ts
120+
import { withClientState } from "apollo-link-state";
121+
122+
const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({
123+
resolvers: [FirstResolver, SecondResolver],
124+
});
125+
126+
const stateLink = withClientState({
127+
// ... Other options like `cache`
128+
typeDefs,
129+
resolvers,
130+
});
131+
132+
// ... Rest of `ApolloClient` initialization code
133+
```
134+
135+
There's also a `sync` version of it - `buildTypeDefsAndResolversSync`:
136+
137+
```ts
138+
const { typeDefs, resolvers } = buildTypeDefsAndResolversSync({
139+
resolvers: [FirstResolver, SecondResolver],
140+
});
141+
```
142+
143+
However, be aware that some of the TypeGraphQL features (i.a. [query complexity](./complexity.md)) might not work with the `buildTypeDefsAndResolvers` approach because they use some low-level `graphql-js` features.

0 commit comments

Comments
 (0)