Skip to content

Commit

Permalink
release: 2.0.0-beta.4
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed Jan 3, 2024
1 parent 299a87d commit e28a028
Show file tree
Hide file tree
Showing 25 changed files with 2,944 additions and 4 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
@@ -1,9 +1,11 @@
# Changelog and release notes

## Unreleased
<!-- ## Unreleased -->

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

## v2.0.0-beta.4

### Features

- **Breaking Change**: expose shim as a package entry point `type-graphql/shim` (and `/node_modules/type-graphql/build/typings/shim.ts`)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "type-graphql",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"private": false,
"description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!",
"keywords": [
Expand Down
195 changes: 195 additions & 0 deletions website/versioned_docs/version-2.0.0-beta.4/authorization.md
@@ -0,0 +1,195 @@
---
title: Authorization
id: version-2.0.0-beta.4-authorization
original_id: authorization
---

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.

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.

That's why authorization is a first-class feature in `TypeGraphQL`!

## How to use

First, we need to use the `@Authorized` decorator as a guard on a field, query or mutation.
Example object type field guards:

```ts
@ObjectType()
class MyObject {
@Field()
publicField: string;

@Authorized()
@Field()
authorizedField: string;

@Authorized("ADMIN")
@Field()
adminField: string;

@Authorized(["ADMIN", "MODERATOR"])
@Field({ nullable: true })
hiddenField?: string;
}
```

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.
By default the roles are of type `string` but they can easily be changed as the decorator is generic - `@Authorized<number>(1, 7, 22)`.

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.

Sample query and mutation guards:

```ts
@Resolver()
class MyResolver {
@Query()
publicQuery(): MyObject {
return {
publicField: "Some public data",
authorizedField: "Data for logged users only",
adminField: "Top secret info for admin",
};
}

@Authorized()
@Query()
authedQuery(): string {
return "Authorized users only!";
}

@Authorized("ADMIN", "MODERATOR")
@Mutation()
adminMutation(): string {
return "You are an admin/moderator, you can safely drop the database ;)";
}
}
```

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`.

Next, we need to create our auth checker function. Its implementation may depend on our business logic:

```ts
export const customAuthChecker: AuthChecker<ContextType> = (
{ root, args, context, info },
roles,
) => {
// Read user from context
// and check the user's permission against the `roles` argument
// that comes from the '@Authorized' decorator, eg. ["ADMIN", "MODERATOR"]

return true; // or 'false' if access is denied
};
```

The second argument of the `AuthChecker` generic type is `RoleType` - used together with the `@Authorized` decorator generic type.

Auth checker can be also defined as a class - this way we can leverage the dependency injection mechanism:

```ts
export class CustomAuthChecker implements AuthCheckerInterface<ContextType> {
constructor(
// Dependency injection
private readonly userRepository: Repository<User>,
) {}

check({ root, args, context, info }: ResolverData<ContextType>, roles: string[]) {
const userId = getUserIdFromToken(context.token);
// Use injected service
const user = this.userRepository.getById(userId);

// Custom logic, e.g.:
return user % 2 === 0;
}
}
```

The last step is to register the function or class while building the schema:

```ts
import { customAuthChecker } from "../auth/custom-auth-checker.ts";

const schema = await buildSchema({
resolvers: [MyResolver],
// Register the auth checking function
// or defining it inline
authChecker: customAuthChecker,
});
```

And it's done! 😉

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"`:

```ts
const schema = await buildSchema({
resolvers: ["./**/*.resolver.ts"],
authChecker: customAuthChecker,
authMode: "null",
});
```

It will then return `null` instead of throwing an authorization error.

## Recipes

We can also use `TypeGraphQL` with JWT authentication.
Here's an example using `@apollo/server`:

```ts
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import express from "express";
import jwt from "express-jwt";
import bodyParser from "body-parser";
import { schema } from "./graphql/schema";
import { User } from "./User.type";

// GraphQL path
const GRAPHQL_PATH = "/graphql";

// GraphQL context
type Context = {
user?: User;
};

// Express
const app = express();

// Apollo server
const server = new ApolloServer<Context>({ schema });
await server.start();

// Mount a JWT or other authentication middleware that is run before the GraphQL execution
app.use(
GRAPHQL_PATH,
jwt({
secret: "TypeGraphQL",
credentialsRequired: false,
}),
);

// Apply GraphQL server middleware
app.use(
GRAPHQL_PATH,
bodyParser.json(),
expressMiddleware(server, {
// Build context
// 'req.user' comes from 'express-jwt'
context: async ({ req }) => ({ user: req.user }),
}),
);

// Start server
await new Promise<void>(resolve => app.listen({ port: 4000 }, resolve));
console.log(`GraphQL server ready at http://localhost:4000/${GRAPHQL_PATH}`);
```

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.

## Example

See how this works in the [simple real life example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.4/examples/authorization).
37 changes: 37 additions & 0 deletions website/versioned_docs/version-2.0.0-beta.4/aws-lambda.md
@@ -0,0 +1,37 @@
---
title: AWS Lambda integration
id: version-2.0.0-beta.4-aws-lambda
original_id: aws-lambda
---

## Using TypeGraphQL in AWS Lambda environment

AWS Lambda environment is a bit different than a standard Node.js server deployment.

However, the only tricky part with the setup is that we need to "cache" the built schema, to save some computing time by avoiding rebuilding the schema on every request to our lambda.

So all we need to do is to assign the built schema to the local variable using the `??=` conditional assignment operator.
We can do the same thing for `ApolloServer`.

Below you you can find the full snippet for the AWS Lambda integration:

```ts
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { ApolloServer } from "apollo-server-lambda";

let cachedSchema: GraphQLSchema | null = null;
let cachedServer: ApolloServer | null = null;

export const handler: APIGatewayProxyHandlerV2 = async (event, context, callback) => {
// build TypeGraphQL executable schema only once, then read it from local "cached" variable
cachedSchema ??= await buildSchema({
resolvers: [RecipeResolver],
});

// create the GraphQL server only once
cachedServer ??= new ApolloServer({ schema: cachedSchema });

// make a handler for `aws-lambda`
return cachedServer.createHandler({})(event, context, callback);
};
```
40 changes: 40 additions & 0 deletions website/versioned_docs/version-2.0.0-beta.4/browser-usage.md
@@ -0,0 +1,40 @@
---
title: Browser usage
id: version-2.0.0-beta.4-browser-usage
original_id: browser-usage
---

## Using classes in a client app

Sometimes we might want to use the classes we've created and annotated with TypeGraphQL decorators, in our client app that works in the browser. For example, reusing the args or input classes with `class-validator` decorators or the object type classes with some helpful custom methods.

Since TypeGraphQL is a Node.js framework, it doesn't work in a browser environment, so we may quickly get an error, e.g. `ERROR in ./node_modules/fs.realpath/index.js` or `utils1_promisify is not a function`, while trying to build our app with Webpack. To correct this, we have to configure Webpack to use the decorator shim instead of the normal module. We simply add this plugin code to our webpack config:

```js
module.exports = {
// ... Rest of Webpack configuration
plugins: [
// ... Other existing plugins
new webpack.NormalModuleReplacementPlugin(/type-graphql$/, resource => {
resource.request = resource.request.replace(/type-graphql/, "type-graphql/shim");
}),
];
}
```

In case of cypress, you can adapt the same webpack config trick just by applying the [cypress-webpack-preprocessor](https://github.com/cypress-io/cypress-webpack-preprocessor) plugin.

However, in some TypeScript projects like the ones using Angular, which AoT compiler requires that a full `*.ts` file is provided instead of just a `*.js` and `*.d.ts` files, to use this shim we have to simply set up our TypeScript configuration in `tsconfig.json` to use this file instead of a normal TypeGraphQL module:

```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"type-graphql": ["./node_modules/type-graphql/build/typings/shim.ts"]
}
}
}
```

Thanks to this, our bundle will be much lighter as we don't need to embed the whole TypeGraphQL library code in our app.

0 comments on commit e28a028

Please sign in to comment.