diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a97b06bb..9bb7f4ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog and release notes -## Unreleased + +## v2.0.0-rc.2 + ## Features - support declaring middlewares on resolver class level (#620) diff --git a/package-lock.json b/package-lock.json index 192769ac4..e8a4cd4e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "type-graphql", - "version": "2.0.0-rc.1", + "version": "2.0.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "type-graphql", - "version": "2.0.0-rc.1", + "version": "2.0.0-rc.2", "funding": [ { "type": "github", diff --git a/package.json b/package.json index b418fd8e5..d45dede74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "type-graphql", - "version": "2.0.0-rc.1", + "version": "2.0.0-rc.2", "private": false, "description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!", "keywords": [ diff --git a/website/i18n/en.json b/website/i18n/en.json index 31cd7fbe9..f3103627d 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -811,6 +811,56 @@ "version-2.0.0-rc.1/version-2.0.0-rc.1-validation": { "title": "Argument and Input validation", "sidebar_label": "Validation" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-authorization": { + "title": "Authorization" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-azure-functions": { + "title": "Azure Functions Integration" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-browser-usage": { + "title": "Browser usage" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-complexity": { + "title": "Query complexity" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-custom-decorators": { + "title": "Custom decorators" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-dependency-injection": { + "title": "Dependency injection" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-examples": { + "title": "Examples", + "sidebar_label": "List of examples" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-extensions": { + "title": "Extensions" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-generic-types": { + "title": "Generic Types" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-inheritance": { + "title": "Inheritance" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-interfaces": { + "title": "Interfaces" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-middlewares": { + "title": "Middleware and guards" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-resolvers": { + "title": "Resolvers" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-subscriptions": { + "title": "Subscriptions" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-unions": { + "title": "Unions" + }, + "version-2.0.0-rc.2/version-2.0.0-rc.2-validation": { + "title": "Argument and Input validation", + "sidebar_label": "Validation" } }, "links": { diff --git a/website/versioned_docs/version-2.0.0-rc.2/authorization.md b/website/versioned_docs/version-2.0.0-rc.2/authorization.md new file mode 100644 index 000000000..8b2e20f08 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/authorization.md @@ -0,0 +1,220 @@ +--- +title: Authorization +id: version-2.0.0-rc.2-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`! + +## Declaration + +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(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`. + +However, declaring `@Authorized()` on all the resolver's class methods would be not only a tedious task but also an error-prone one, as it's easy to forget to put it on some newly added method, etc. +Hence, TypeGraphQL support declaring `@Authorized()` or the resolver class level. This way you can declare it once per resolver's class but you can still overwrite the defaults and narrows the authorization rules: + +```ts +@Authorized() +@Resolver() +class MyResolver { + // this will inherit the auth guard defined on the class level + @Query() + authedQuery(): string { + return "Authorized users only!"; + } + + // this one overwrites the resolver's one + // and registers roles required for this mutation + @Authorized("ADMIN", "MODERATOR") + @Mutation() + adminMutation(): string { + return "You are an admin/moderator, you can safely drop the database ;)"; + } +} +``` + +## Runtime checks + +Having all the metadata for authorization set, we need to create our auth checker function. Its implementation may depend on our business logic: + +```ts +export const customAuthChecker: AuthChecker = ( + { 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 { + constructor( + // Dependency injection + private readonly userRepository: Repository, + ) {} + + check({ root, args, context, info }: ResolverData, 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({ 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(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-rc.2/examples/authorization). diff --git a/website/versioned_docs/version-2.0.0-rc.2/azure-functions.md b/website/versioned_docs/version-2.0.0-rc.2/azure-functions.md new file mode 100644 index 000000000..532378eb7 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/azure-functions.md @@ -0,0 +1,120 @@ +--- +title: Azure Functions Integration +id: version-2.0.0-rc.2-azure-functions +original_id: azure-functions +--- + +## Using TypeGraphQL in Microsoft Azure Functions + +Integrating TypeGraphQL with Azure Functions involves the following key steps: + +1. Generate GraphQL schema based on your resolvers +2. Notify Apollo Server about your schema + +Below is how you can implement the azure function entry point (with explanations in-line): + +```ts +// index.ts + +import "reflect-metadata"; +import path from "path"; +import { ApolloServer } from "@apollo/server"; +import { startServerAndCreateHandler } from "@as-integrations/azure-functions"; +import { buildSchemaSync } from "type-graphql"; +import { Container } from "typedi"; +import { GraphQLFormattedError } from "graphql"; +import { UserResolver } from "YOUR_IMPORT_PATH"; // TypeGraphQL Resolver +import { AccountResolver } from "YOUR_IMPORT_PATH"; // TypeGraphQL Resolver + +// Bundle resolvers to build the schema +const schema = buildSchemaSync({ + // Include resolvers you'd like to expose to the API + // Deployment to Azure functions might fail if + // you include too much resolvers (means your app is too big) + resolvers: [ + UserResolver, + AccountResolver, + // your other resolvers + ], + + // Only build the GraphQL schema locally + // The resulting schema.graphql will be generated to the following path: + // Path: /YOUR_PROJECT/src/schema.graphql + emitSchemaFile: process.env.NODE_ENV === "local" ? path.resolve("./src/schema.graphql") : false, + container: Container, + validate: true, +}); + +// Add schema into Apollo Server +const server = new ApolloServer({ + // include your schema + schema, + + // only allow introspection in non-prod environments + introspection: process.env.NODE_ENV !== "production", + + // you can handle errors in your own styles + formatError: (err: GraphQLFormattedError) => err, +}); + +// Start the server(less handler/function) +export default startServerAndCreateHandler(server); +``` + +Each Azure Function needs to have an equivalent configuration file called `function.json`, here's how you can configure it: + +```json +// function.json + +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "graphql", + "methods": ["get", "post", "options"] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "scriptFile": "../dist/handler-graphql/index.js" +} +``` + +For better maintainability of your codebase, we recommend separate your Azure Functions into its own folders, away from the actual GraphQL Resolvers. Here's an example: + +```text +/YOUR_PROJECT + /handlers + /handler-graphql + index.ts + function.json + /handler-SOME-OTHER-FUNCTION-1 + index.ts + function.json + /handler-SOME-OTHER-FUNCTION-2 + index.ts + function.json + + /src + /resolvers + user.resolver.ts + account.resolver.ts + /services + user.service.ts + account.service.ts + + package.json + host.json + .eslintrc.js + .prettierrc + .eslintignore + .prettierignore + +etc etc etc... +``` diff --git a/website/versioned_docs/version-2.0.0-rc.2/browser-usage.md b/website/versioned_docs/version-2.0.0-rc.2/browser-usage.md new file mode 100644 index 000000000..36b905a34 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/browser-usage.md @@ -0,0 +1,66 @@ +--- +title: Browser usage +id: version-2.0.0-rc.2-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 e.g. with Webpack. To correct this, we have to configure bundler or compiler to use the decorator shim instead of the normal module. + +The steps to accomplish this are different, depending on the framework, bundler or compiler we use. +However, in all cases, using shim makes our bundle much lighter as we don't need to embed the whole TypeGraphQL library code in our app. + +## CRA and similar + +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, we can adapt the same webpack config trick just by applying the [cypress-webpack-preprocessor](https://github.com/cypress-io/cypress-webpack-preprocessor) plugin. + +## Angular and similar + +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"] + } + } +} +``` + +## Next.js and similar + +When using the shim with Next.js as a dedicated frontend server be aware that Next has pre-renders on the server. This means that in development mode the `webpack: {}` config in `next.config.js` is skipped and full `type-graphql` is bundled. But we still need to handle some webpack rewiring for the client bundling which still happens with webpack both in development and in production mode. + +The easiest way is to accomplish this is also done in `tsconfig.json` - add the same keys like in the example before to `compilerOptions`: + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "type-graphql": ["node_modules/type-graphql/build/typings/shim.ts"] + } + } +} +``` + +Then, `npm install -D tsconfig-paths` and enable it with `NODE_OPTIONS="-r tsconfig-paths/register"` in our environment variables setup. diff --git a/website/versioned_docs/version-2.0.0-rc.2/complexity.md b/website/versioned_docs/version-2.0.0-rc.2/complexity.md new file mode 100644 index 000000000..67607a80b --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/complexity.md @@ -0,0 +1,103 @@ +--- +title: Query complexity +id: version-2.0.0-rc.2-complexity +original_id: complexity +--- + +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). + +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`. + +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. + +## How to use + +First, we need to pass `complexity` as an option to the decorator on a field, query or mutation. + +Example of complexity + +```ts +@ObjectType() +class MyObject { + @Field({ complexity: 2 }) + publicField: string; + + @Field({ complexity: ({ args, childComplexity }) => childComplexity + 1 }) + complexField: string; +} +``` + +The `complexity` option may be omitted if the complexity value is 1. +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. + +In the next step, we will integrate `graphql-query-complexity` with the server that expose our GraphQL schema over HTTP. +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: + +```ts +async function bootstrap() { + // ... Build GraphQL schema + + // Create GraphQL server + const server = new ApolloServer({ + schema, + // Create a plugin to allow query complexity calculation for every request + plugins: [ + { + requestDidStart: async () => ({ + async didResolveOperation({ request, document }) { + /** + * Provides GraphQL query analysis to be able to react on complex queries to the GraphQL server + * It can be used to protect the GraphQL server against resource exhaustion and DoS attacks + * More documentation can be found at https://github.com/ivome/graphql-query-complexity + */ + const complexity = getComplexity({ + // GraphQL schema + schema, + // To calculate query complexity properly, + // check only the requested operation + // not the whole document that may contains multiple operations + operationName: request.operationName, + // GraphQL query document + query: document, + // GraphQL query variables + variables: request.variables, + // Add any number of estimators. The estimators are invoked in order, the first + // numeric value that is being returned by an estimator is used as the field complexity + // If no estimator returns a value, an exception is raised + estimators: [ + // Using fieldExtensionsEstimator is mandatory to make it work with type-graphql + fieldExtensionsEstimator(), + // Add more estimators here... + // This will assign each field a complexity of 1 + // if no other estimator returned a value + simpleEstimator({ defaultComplexity: 1 }), + ], + }); + + // React to the calculated complexity, + // like compare it with max and throw error when the threshold is reached + if (complexity > MAX_COMPLEXITY) { + throw new Error( + `Sorry, too complicated query! ${complexity} exceeded the maximum allowed complexity of ${MAX_COMPLEXITY}`, + ); + } + console.log("Used query complexity points:", complexity); + }, + }), + }, + ], + }); + + // Start server + const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); + console.log(`GraphQL server ready at ${url}`); +} +``` + +And it's done! πŸ˜‰ + +For more info about how query complexity is computed, please visit [graphql-query-complexity](https://github.com/ivome/graphql-query-complexity). + +## Example + +See how this works in the [simple query complexity example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/query-complexity). diff --git a/website/versioned_docs/version-2.0.0-rc.2/custom-decorators.md b/website/versioned_docs/version-2.0.0-rc.2/custom-decorators.md new file mode 100644 index 000000000..18386cf32 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/custom-decorators.md @@ -0,0 +1,186 @@ +--- +title: Custom decorators +id: version-2.0.0-rc.2-custom-decorators +original_id: custom-decorators +--- + +Custom decorators are a great way to reduce the boilerplate and reuse some common logic between different resolvers. TypeGraphQL supports three kinds of custom decorators - method, resolver class and parameter. + +## Method decorators + +Using [middlewares](./middlewares.md) allows to reuse some code between resolvers. To further reduce the boilerplate and have a nicer API, we can create our own custom method decorators. + +They work in the same way as the [reusable middleware function](./middlewares.md#reusable-middleware), however, in this case we need to call `createMethodMiddlewareDecorator` helper function with our middleware logic and return its value: + +```ts +export function ValidateArgs(schema: JoiSchema) { + return createMethodMiddlewareDecorator(async ({ args }, next) => { + // Middleware code that uses custom decorator arguments + + // e.g. Validation logic based on schema using 'joi' + await joiValidate(schema, args); + return next(); + }); +} +``` + +The usage is then very simple, as we have a custom, descriptive decorator - we just place it above the resolver/field and pass the required arguments to it: + +```ts +@Resolver() +export class RecipeResolver { + @ValidateArgs(MyArgsSchema) // Custom decorator + @UseMiddleware(ResolveTime) // Explicit middleware + @Query() + randomValue(@Args() { scale }: MyArgs): number { + return Math.random() * scale; + } +} +``` + +## Resolver class decorators + +Similar to method decorators, we can create our own custom resolver class decorators. +In this case we need to call `createResolverClassMiddlewareDecorator` helper function, just like we did for `createMethodMiddlewareDecorator`: + +```ts +export function ValidateArgs(schema: JoiSchema) { + return createResolverClassMiddlewareDecorator(async ({ args }, next) => { + // Middleware code that uses custom decorator arguments + + // e.g. Validation logic based on schema using 'joi' + await joiValidate(schema, args); + return next(); + }); +} +``` + +The usage is then analogue - we just place it above the resolver class and pass the required arguments to it: + +```ts +@ValidateArgs(MyArgsSchema) // Custom decorator +@UseMiddleware(ResolveTime) // Explicit middleware +@Resolver() +export class RecipeResolver { + @Query() + randomValue(@Args() { scale }: MyArgs): number { + return Math.random() * scale; + } +} +``` + +This way, we just need to put it once in the code and our custom decorator will be applied to all the resolver's queries or mutations. As simple as that! + +## Parameter decorators + +Parameter decorators are just like the custom method decorators or middlewares but with an ability to return some value that will be injected to the method as a parameter. Thanks to this, it reduces the pollution in `context` which was used as a workaround for the communication between reusable middlewares and resolvers. + +They might be just a simple data extractor function, that makes our resolver more unit test friendly: + +```ts +function CurrentUser() { + return createParameterDecorator(({ context }) => context.currentUser); +} +``` + +Or might be a more advanced one that performs some calculations and encapsulates some logic. Compared to middlewares, they allow for a more granular control on executing the code, like calculating fields map based on GraphQL info only when it's really needed (requested by using the `@Fields()` decorator): + +```ts +function Fields(level = 1): ParameterDecorator { + return createParameterDecorator(async ({ info }) => { + const fieldsMap: FieldsMap = {}; + // Calculate an object with info about requested fields + // based on GraphQL 'info' parameter of the resolver and the level parameter + // or even call some async service, as it can be a regular async function and we can just 'await' + return fieldsMap; + }); +} +``` + +> Be aware, that `async` function as a custom param decorators logic can make the GraphQL resolver execution slower, so try to avoid them, if possible. + +Then we can use our custom param decorators in the resolvers just like the built-in decorators: + +```ts +@Resolver() +export class RecipeResolver { + constructor(private readonly recipesRepository: Repository) {} + + @Authorized() + @Mutation(returns => Recipe) + async addRecipe( + @Args() recipeData: AddRecipeInput, + // Custom decorator just like the built-in one + @CurrentUser() currentUser: User, + ) { + const recipe: Recipe = { + ...recipeData, + // and use the data returned from custom decorator in the resolver code + author: currentUser, + }; + await this.recipesRepository.save(recipe); + + return recipe; + } + + @Query(returns => Recipe, { nullable: true }) + async recipe( + @Arg("id") id: string, + // Custom decorator that parses the fields from GraphQL query info + @Fields() fields: FieldsMap, + ) { + return await this.recipesRepository.find(id, { + // use the fields map as a select projection to optimize db queries + select: fields, + }); + } +} +``` + +### Custom `@Arg` decorator + +In some cases we might want to create a custom decorator that will also register/expose an argument in the GraphQL schema. +Calling both `Arg()` and `createParameterDecorator()` inside a custom decorator does not play well with the internals of TypeGraphQL. + +Hence, the `createParameterDecorator()` function supports second argument, `CustomParameterOptions` which allows to set decorator metadata for `@Arg` under the `arg` key: + +```ts +function RandomIdArg(argName = "id") { + return createParameterDecorator( + // here we do the logic of getting provided argument or generating a random one + ({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE), + { + // here we provide the metadata to register the parameter as a GraphQL argument + arg: { + name: argName, + typeFunc: () => Int, + options: { + nullable: true, + description: "Accepts provided id or generates a random one.", + }, + }, + }, + ); +} +``` + +The usage of that custom decorator is very similar to the previous one and `@Arg` decorator itself: + +```ts +@Resolver() +export class RecipeResolver { + constructor(private readonly recipesRepository: Repository) {} + + @Query(returns => Recipe, { nullable: true }) + async recipe( + // custom decorator that will expose an arg in the schema + @RandomIdArg("id") id: number, + ) { + return await this.recipesRepository.findById(id); + } +} +``` + +## Example + +See how different kinds of custom decorators work in the [custom decorators and middlewares example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/middlewares-custom-decorators). diff --git a/website/versioned_docs/version-2.0.0-rc.2/dependency-injection.md b/website/versioned_docs/version-2.0.0-rc.2/dependency-injection.md new file mode 100644 index 000000000..6978cf22d --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/dependency-injection.md @@ -0,0 +1,180 @@ +--- +title: Dependency injection +id: version-2.0.0-rc.2-dependency-injection +original_id: dependency-injection +--- + +Dependency injection is a really useful pattern that helps in decoupling parts of the app. + +TypeGraphQL supports this technique by allowing users to provide their IoC container that will be used by the framework. + +## Basic usage + +The usage of this feature is very simple - all you need to do is register a 3rd party container. + +Example using TypeDI: + +```ts +import { buildSchema } from "type-graphql"; +// IOC container +import { Container } from "typedi"; +import { SampleResolver } from "./resolvers"; + +// Build TypeGraphQL executable schema +const schema = await buildSchema({ + // Array of resolvers + resolvers: [SampleResolver], + // Registry 3rd party IOC container + container: Container, +}); +``` + +Resolvers will then be able to declare their dependencies and TypeGraphQL will use the container to solve them: + +```ts +import { Service } from "typedi"; + +@Service() +@Resolver(of => Recipe) +export class RecipeResolver { + constructor( + // Dependency injection + private readonly recipeService: RecipeService, + ) {} + + @Query(returns => Recipe, { nullable: true }) + async recipe(@Arg("recipeId") recipeId: string) { + // Usage of the injected service + return this.recipeService.getOne(recipeId); + } +} +``` + +A sample recipe service implementation may look like this: + +```ts +import { Service, Inject } from "typedi"; + +@Service() +export class RecipeService { + @Inject("SAMPLE_RECIPES") + private readonly items: Recipe[], + + async getAll() { + return this.items; + } + + async getOne(id: string) { + return this.items.find(item => item.id === id); + } +} +``` + +> Be aware than when you use [InversifyJS](https://github.com/inversify/InversifyJS), you have to bind the resolver class with the [self-binding of concrete types](https://github.com/inversify/InversifyJS/blob/master/wiki/classes_as_id.md#self-binding-of-concrete-types), e.g.: +> +> ```ts +> container.bind(SampleResolver).to(SampleResolver).inSingletonScope(); +> ``` + +## Scoped containers + +Dependency injection is a really powerful pattern, but some advanced users may encounter the need for creating fresh instances of some services or resolvers for every request. Since `v0.13.0`, **TypeGraphQL** supports this feature, that is extremely useful for tracking logs by individual requests or managing stateful services. + +To register a scoped container, we need to make some changes in the server bootstrapping config code. +First we need to provide a container resolver function. It takes the resolver data (like context) as an argument and should return an instance of the container scoped to the request. + +For simple container libraries we may define it inline, e.g. using `TypeDI`: + +```ts +await buildSchema({ + container: (({ context }: ResolverData) => Container.of(context.requestId)); +}; +``` + +The tricky part is where the `context.requestId` comes from. Unfortunately, we need to provide it manually using hooks that are exposed by HTTP GraphQL middleware like `express-graphql`, `@apollo/server` or `graphql-yoga`. + +For some other advanced libraries, we might need to create an instance of the container, place it in the context object and then retrieve it in the `container` getter function: + +```ts +await buildSchema({ + container: (({ context }: ResolverData) => context.container); +}; +``` + +Example using `TypeDI` and `@apollo/server` with the `context` creation method: + +```ts +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { Container } from "typedi"; + +// Create GraphQL server +const server = new ApolloServer({ + // GraphQL schema + schema, +}); + +// Start server +const { url } = await startStandaloneServer(server, { + listen: { port: 4000 }, + // Provide unique context with 'requestId' for each request + context: async () => { + const requestId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // uuid-like + const container = Container.of(requestId.toString()); // Get scoped container + const context = { requestId, container }; // Create context + container.set("context", context); // Set context or other data in container + + return context; + }, +}); +console.log(`GraphQL server ready at ${url}`); +``` + +We also have to dispose the container after the request has been handled and the response is ready. Otherwise, there would be a huge memory leak as the new instances of services and resolvers have been created for each request but they haven't been cleaned up. + +Apollo Server has a [plugins](https://www.apollographql.com/docs/apollo-server/integrations/plugins) feature that supports [`willSendResponse`](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#willsendresponse) lifecycle event. We can leverage it to clean up the container after handling the request. + +Example using `TypeDI` and `@apollo/server` with plugins approach: + +```ts +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { Container } from "typedi"; + +const server = new ApolloServer({ + // GraphQL schema + schema, + // Create a plugin to allow for disposing the scoped container created for every request + plugins: [ + { + requestDidStart: async () => ({ + async willSendResponse(requestContext) { + // Dispose the scoped container to prevent memory leaks + Container.reset(requestContext.contextValue.requestId.toString()); + + // For developers curiosity purpose, here is the logging of current scoped container instances + // Make multiple parallel requests to see in console how this works + const instancesIds = ((Container as any).instances as ContainerInstance[]).map( + instance => instance.id, + ); + console.log("Instances left in memory: ", instancesIds); + }, + }), + }, + ], +}); +``` + +And basically that's it! The configuration of the container is done and TypeGraphQL will be able to use different instances of resolvers for each request. + +The only thing that's left is the container configuration - we need to check out the docs for our container library (`InversifyJS`, `injection-js`, `TypeDI` or other) to get know how to setup the lifetime of the injectable objects (transient, scoped or singleton). + +> Be aware that some libraries (like `TypeDI`) by default create new instances for every scoped container, so you might experience a **significant increase in memory usage** and some slowing down in query resolving speed, so please be careful with using this feature! + +## Example + +You can see how this fits together in the [simple example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/using-container). + +For a more advanced usage example with scoped containers, check out [advanced example with scoped containers](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/using-scoped-container). + +Integration with [TSyringe](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/tsyringe). diff --git a/website/versioned_docs/version-2.0.0-rc.2/examples.md b/website/versioned_docs/version-2.0.0-rc.2/examples.md new file mode 100644 index 000000000..55db211b3 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/examples.md @@ -0,0 +1,53 @@ +--- +title: Examples +sidebar_label: List of examples +id: version-2.0.0-rc.2-examples +original_id: examples +--- + +On the [GitHub repository](https://github.com/MichalLytek/type-graphql) there are a few simple [`examples`](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples) of how to use different `TypeGraphQL` features and how well they integrate with 3rd party libraries. + +To run an example, simply go to the subdirectory (e.g. `cd ./simple-usage`), and then start the server (`npx ts-node ./index.ts`). + +Each subdirectory contains a `examples.graphql` file with predefined GraphQL queries/mutations/subscriptions that you can use in Apollo Studio () and play with them by modifying their shape and data. + +## Basics + +- [Simple usage of fields, basic types and resolvers](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/simple-usage) + +## Advanced + +- [Enums and unions](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/enums-and-unions) +- [Subscriptions (simple)](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/simple-subscriptions) +- [Subscriptions (using Redis) \*\*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/redis-subscriptions) +- [Interfaces](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/interfaces-inheritance) +- [Extensions (metadata)](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/extensions) + +## Features usage + +- [Dependency injection (IoC container)](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/using-container) + - [Scoped containers](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/using-scoped-container) +- [Authorization](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/authorization) +- [Validation](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/automatic-validation) + - [Custom validation](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/custom-validation) +- [Types inheritance](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/interfaces-inheritance) +- [Resolvers inheritance](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/resolvers-inheritance) +- [Generic types](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/generic-types) +- [Mixin classes](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/mixin-classes) +- [Middlewares and Custom Decorators](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/middlewares-custom-decorators) +- [Query complexity](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/query-complexity) + +## 3rd party libs integration + +- [TypeORM (manual, synchronous) \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/typeorm-basic-usage) +- [TypeORM (automatic, lazy relations) \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/typeorm-lazy-relations) +- [MikroORM \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/mikro-orm) +- [Typegoose \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/typegoose) +- [Apollo Federation](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/apollo-federation) +- [Apollo Federation 2](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/apollo-federation-2) +- [Apollo Cache Control](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/apollo-cache) +- [GraphQL Scalars](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/graphql-scalars) +- [TSyringe](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/tsyringe) + +_\* Note that we need to provide the environment variable `DATABASE_URL` with connection parameters to your local database_ \ +_\*\* Note that we need to provide the environment variable `REDIS_URL` with connection parameters to your local Redis instance_ diff --git a/website/versioned_docs/version-2.0.0-rc.2/extensions.md b/website/versioned_docs/version-2.0.0-rc.2/extensions.md new file mode 100644 index 000000000..5a94daabd --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/extensions.md @@ -0,0 +1,119 @@ +--- +title: Extensions +id: version-2.0.0-rc.2-extensions +original_id: extensions +--- + +The `graphql-js` library allows for putting arbitrary data into GraphQL types config inside the `extensions` property. +Annotating schema types or fields with a custom metadata, that can be then used at runtime by middlewares or resolvers, is a really powerful and useful feature. + +For such use cases, **TypeGraphQL** provides the `@Extensions` decorator, which adds the data we defined to the `extensions` property of the executable schema for the decorated classes, methods or properties. + +> Be aware that this is a low-level decorator and you generally have to provide your own logic to make use of the `extensions` metadata. + +## Using the `@Extensions` decorator + +Adding extensions to the schema type is as simple as using the `@Extensions` decorator and passing it an object of the custom data we want: + +```ts +@Extensions({ complexity: 2 }) +``` + +We can pass several fields to the decorator: + +```ts +@Extensions({ logMessage: "Restricted access", logLevel: 1 }) +``` + +And we can also decorate a type several times. The snippet below shows that this attaches the exact same extensions data to the schema type as the snippet above: + +```ts +@Extensions({ logMessage: "Restricted access" }) +@Extensions({ logLevel: 1 }) +``` + +If we decorate the same type several times with the same extensions key, the one defined at the bottom takes precedence: + +```ts +@Extensions({ logMessage: "Restricted access" }) +@Extensions({ logMessage: "Another message" }) +``` + +The above usage results in your GraphQL type having a `logMessage: "Another message"` property in its extensions. + +TypeGraphQL classes with the following decorators can be annotated with `@Extensions` decorator: + +- `@ObjectType` +- `@InputType` +- `@Field` +- `@Query` +- `@Mutation` +- `@FieldResolver` + +So the `@Extensions` decorator can be placed over the class property/method or over the type class itself, and multiple times if necessary, depending on what we want to do with the extensions data: + +```ts +@Extensions({ roles: ["USER"] }) +@ObjectType() +class Foo { + @Field() + field: string; +} + +@ObjectType() +class Bar { + @Extensions({ roles: ["USER"] }) + @Field() + field: string; +} + +@ObjectType() +class Bar { + @Extensions({ roles: ["USER"] }) + @Extensions({ visible: false, logMessage: "User accessed restricted field" }) + @Field() + field: string; +} + +@Resolver(of => Foo) +class FooBarResolver { + @Extensions({ roles: ["USER"] }) + @Query() + foobar(@Arg("baz") baz: string): string { + return "foobar"; + } + + @Extensions({ roles: ["ADMIN"] }) + @FieldResolver() + bar(): string { + return "foobar"; + } +} +``` + +## Using the extensions data in runtime + +Once we have decorated the necessary types with extensions, the executable schema will contain the extensions data, and we can make use of it in any way we choose. The most common use will be to read it at runtime in resolvers or middlewares and perform some custom logic there. + +Here is a simple example of a global middleware that will be logging a message on field resolver execution whenever the field is decorated appropriately with `@Extensions`: + +```ts +export class LoggerMiddleware implements MiddlewareInterface { + constructor(private readonly logger: Logger) {} + + use({ info }: ResolverData, next: NextFn) { + // extract `extensions` object from GraphQLResolveInfo object to get the `logMessage` value + const { logMessage } = info.parentType.getFields()[info.fieldName].extensions || {}; + + if (logMessage) { + this.logger.log(logMessage); + } + + return next(); + } +} +``` + +## Examples + +You can see more detailed examples of usage [here](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/extensions). diff --git a/website/versioned_docs/version-2.0.0-rc.2/generic-types.md b/website/versioned_docs/version-2.0.0-rc.2/generic-types.md new file mode 100644 index 000000000..413775add --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/generic-types.md @@ -0,0 +1,169 @@ +--- +title: Generic Types +id: version-2.0.0-rc.2-generic-types +original_id: generic-types +--- + +[Type Inheritance](./inheritance.md) is a great way to reduce code duplication by extracting common fields to the base class. But in some cases, the strict set of fields is not enough because we might need to declare the types of some fields in a more flexible way, like a type parameter (e.g. `items: T[]` in case of a pagination). + +Hence TypeGraphQL also has support for describing generic GraphQL types. + +## How to? + +Unfortunately, the limited reflection capabilities of TypeScript don't allow for combining decorators with standard generic classes. To achieve behavior like that of generic types, we use the same class-creator pattern like the one described in the [Resolvers Inheritance](./inheritance.md) docs. + +### Basic usage + +Start by defining a `PaginatedResponse` function that creates and returns an abstract `PaginatedResponseClass`: + +```ts +export default function PaginatedResponse() { + abstract class PaginatedResponseClass { + // ... + } + return PaginatedResponseClass; +} +``` + +To achieve generic-like behavior, the function has to be generic and take some runtime argument related to the type parameter: + +```ts +export default function PaginatedResponse(TItemClass: ClassType) { + abstract class PaginatedResponseClass { + // ... + } + return PaginatedResponseClass; +} +``` + +Then, add proper decorators to the class which might be `@ObjectType`, `@InterfaceType` or `@InputType`: + +```ts +export default function PaginatedResponse(TItemClass: ClassType) { + @ObjectType() + abstract class PaginatedResponseClass { + // ... + } + return PaginatedResponseClass; +} +``` + +After that, add fields like in a normal class but using the generic type and parameters: + +```ts +export default function PaginatedResponse(TItemClass: ClassType) { + @ObjectType() + abstract class PaginatedResponseClass { + // Runtime argument + @Field(type => [TItemClass]) + // Generic type + items: TItem[]; + + @Field(type => Int) + total: number; + + @Field() + hasMore: boolean; + } + return PaginatedResponseClass; +} +``` + +Finally, use the generic function factory to create a dedicated type class: + +```ts +@ObjectType() +class PaginatedUserResponse extends PaginatedResponse(User) { + // Add more fields or overwrite the existing one's types + @Field(type => [String]) + otherInfo: string[]; +} +``` + +And then use it in our resolvers: + +```ts +@Resolver() +class UserResolver { + @Query() + users(): PaginatedUserResponse { + // Custom business logic, + // depending on underlying data source and libraries + return { + items, + total, + hasMore, + otherInfo, + }; + } +} +``` + +### Complex generic type values + +When we need to provide something different than a class (object type) for the field type, we need to enhance the parameter type signature and provide the needed types. + +Basically, the parameter that the `PaginatedResponse` function accepts is the value we can provide to `@Field` decorator. +So if we want to return an array of strings as the `items` field, we need to add proper types to the function signature, like `GraphQLScalarType` or `String`: + +```ts +export default function PaginatedResponse( + itemsFieldValue: ClassType | GraphQLScalarType | String | Number | Boolean, +) { + @ObjectType() + abstract class PaginatedResponseClass { + @Field(type => [itemsFieldValue]) + items: TItemsFieldValue[]; + + // ... Other fields + } + return PaginatedResponseClass; +} +``` + +And then provide a proper runtime value (like `String`) while creating a proper subtype of generic `PaginatedResponse` object type: + +```ts +@ObjectType() +class PaginatedStringsResponse extends PaginatedResponse(String) { + // ... +} +``` + +### Types factory + +We can also create a generic class without using the `abstract` keyword. +But with this approach, types created with this kind of factory will be registered in the schema, so this way is not recommended to extend the types for adding fields. + +To avoid generating schema errors of duplicated `PaginatedResponseClass` type names, we must provide our own unique, generated type name: + +```ts +export default function PaginatedResponse(TItemClass: ClassType) { + // Provide a unique type name used in schema + @ObjectType(`Paginated${TItemClass.name}Response`) + class PaginatedResponseClass { + // ... + } + return PaginatedResponseClass; +} +``` + +Then, we can store the generated class in a variable and in order to use it both as a runtime object and as a type, we must also create a type for this new class: + +```ts +const PaginatedUserResponse = PaginatedResponse(User); +type PaginatedUserResponse = InstanceType; + +@Resolver() +class UserResolver { + // Provide a runtime type argument to the decorator + @Query(returns => PaginatedUserResponse) + users(): PaginatedUserResponse { + // Same implementation as in the earlier code snippet + } +} +``` + +## Examples + +A more advanced usage example of the generic types feature can be found in [this examples folder](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/generic-types). diff --git a/website/versioned_docs/version-2.0.0-rc.2/inheritance.md b/website/versioned_docs/version-2.0.0-rc.2/inheritance.md new file mode 100644 index 000000000..01c9385c4 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/inheritance.md @@ -0,0 +1,145 @@ +--- +title: Inheritance +id: version-2.0.0-rc.2-inheritance +original_id: inheritance +--- + +The main idea of TypeGraphQL is to create GraphQL types based on TypeScript classes. + +In object-oriented programming it is common to compose classes using inheritance. Hence, TypeGraphQL supports composing type definitions by extending classes. + +## Types inheritance + +One of the most known principles of software development is DRY - Don't Repeat Yourself - which is about avoiding code redundancy. + +While creating a GraphQL API, it's a common pattern to have pagination args in resolvers, like `skip` and `take`. So instead of repeating ourselves, we declare it once: + +```ts +@ArgsType() +class PaginationArgs { + @Field(type => Int) + skip: number = 0; + + @Field(type => Int) + take: number = 25; +} +``` + +and then reuse it everywhere: + +```ts +@ArgsType() +class GetTodosArgs extends PaginationArgs { + @Field() + onlyCompleted: boolean = false; +} +``` + +This technique also works with input type classes, as well as with object type classes: + +```ts +@ObjectType() +class Person { + @Field() + age: number; +} + +@ObjectType() +class Student extends Person { + @Field() + universityName: string; +} +``` + +Note that both the subclass and the parent class must be decorated with the same type of decorator, like `@ObjectType()` in the example `Person -> Student` above. Mixing decorator types across parent and child classes is prohibited and might result in a schema building error, e.g. we can't decorate the subclass with `@ObjectType()` and the parent with `@InputType()`. + +## Resolver Inheritance + +A special kind of inheritance in TypeGraphQL is resolver class inheritance. This pattern allows us e.g. to create a base CRUD resolver class for our resource/entity, so we don't have to repeat common boilerplate code. + +Since we need to generate unique query/mutation names, we have to create a factory function for our base class: + +```ts +function createBaseResolver() { + abstract class BaseResolver {} + + return BaseResolver; +} +``` + +Be aware that with some `tsconfig.json` settings (like `declarations: true`) we might receive a `[ts] Return type of exported function has or is using private name 'BaseResolver'` error - in this case we might need to use `any` as the return type or create a separate class/interface describing the class methods and properties. + +This factory should take a parameter that we can use to generate the query/mutation names, as well as the type that we would return from the resolvers: + +```ts +function createBaseResolver(suffix: string, objectTypeCls: T) { + abstract class BaseResolver {} + + return BaseResolver; +} +``` + +It's very important to mark the `BaseResolver` class using the `@Resolver` decorator: + +```ts +function createBaseResolver(suffix: string, objectTypeCls: T) { + @Resolver() + abstract class BaseResolver {} + + return BaseResolver; +} +``` + +We can then implement the resolver methods as usual. The only difference is that we can use the `name` decorator option for `@Query`, `@Mutation` and `@Subscription` decorators to overwrite the name that will be emitted in schema: + +```ts +function createBaseResolver(suffix: string, objectTypeCls: T) { + @Resolver() + abstract class BaseResolver { + protected items: T[] = []; + + @Query(type => [objectTypeCls], { name: `getAll${suffix}` }) + async getAll(@Arg("first", type => Int) first: number): Promise { + return this.items.slice(0, first); + } + } + + return BaseResolver; +} +``` + +Now we can create a specific resolver class that will extend the base resolver class: + +```ts +const PersonBaseResolver = createBaseResolver("person", Person); + +@Resolver(of => Person) +export class PersonResolver extends PersonBaseResolver { + // ... +} +``` + +We can also add specific queries and mutations in our resolver class, as always: + +```ts +const PersonBaseResolver = createBaseResolver("person", Person); + +@Resolver(of => Person) +export class PersonResolver extends PersonBaseResolver { + @Mutation() + addPerson(@Arg("input") personInput: PersonInput): Person { + this.items.push(personInput); + return personInput; + } +} +``` + +And that's it! We just need to normally register `PersonResolver` in `buildSchema` and the extended resolver will work correctly. + +We must be aware that if we want to overwrite the query/mutation/subscription from the parent resolver class, we need to generate the same schema name (using the `name` decorator option or the class method name). It will overwrite the implementation along with the GraphQL args and return types. If we only provide a different implementation of the inherited method like `getOne`, it won't work. + +## Examples + +More advanced usage examples of type inheritance (and interfaces) can be found in [the example folder](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/interfaces-inheritance). + +For a more advanced resolver inheritance example, please go to [this example folder](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/resolvers-inheritance). diff --git a/website/versioned_docs/version-2.0.0-rc.2/interfaces.md b/website/versioned_docs/version-2.0.0-rc.2/interfaces.md new file mode 100644 index 000000000..446790e0f --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/interfaces.md @@ -0,0 +1,258 @@ +--- +title: Interfaces +id: version-2.0.0-rc.2-interfaces +original_id: interfaces +--- + +The main idea of TypeGraphQL is to create GraphQL types based on TypeScript classes. + +In object-oriented programming it is common to create interfaces which describe the contract that classes implementing them must adhere to. Hence, TypeGraphQL supports defining GraphQL interfaces. + +Read more about the GraphQL Interface Type in the [official GraphQL docs](https://graphql.org/learn/schema/#interfaces). + +## Abstract classes + +TypeScript has first class support for interfaces. Unfortunately, they only exist at compile-time, so we can't use them to build GraphQL schema at runtime by using decorators. + +Luckily, we can use an abstract class for this purpose. It behaves almost like an interface as it can't be instantiated but it can be implemented by another class. The only difference is that it just won't prevent developers from implementing a method or initializing a field. So, as long as we treat the abstract class like an interface, we can safely use it. + +## Defining interface type + +How do we create a GraphQL interface definition? We create an abstract class and decorate it with the `@InterfaceType()` decorator. The rest is exactly the same as with object types: we use the `@Field` decorator to declare the shape of the type: + +```ts +@InterfaceType() +abstract class IPerson { + @Field(type => ID) + id: string; + + @Field() + name: string; + + @Field(type => Int) + age: number; +} +``` + +We can then use this interface type class like an interface in the object type class definition: + +```ts +@ObjectType({ implements: IPerson }) +class Person implements IPerson { + id: string; + name: string; + age: number; +} +``` + +The only difference is that we have to let TypeGraphQL know that this `ObjectType` is implementing the `InterfaceType`. We do this by passing the param `({ implements: IPerson })` to the decorator. If we implement multiple interfaces, we pass an array of interfaces like so: `({ implements: [IPerson, IAnimal, IMachine] })`. + +It is also allowed to omit the decorators since the GraphQL types will be copied from the interface definition - this way we won't have to maintain two definitions and solely rely on TypeScript type checking for correct interface implementation. + +We can also extend the base interface type abstract class as well because all the fields are inherited and emitted in schema: + +```ts +@ObjectType({ implements: IPerson }) +class Person extends IPerson { + @Field() + hasKids: boolean; +} +``` + +## Implementing other interfaces + +Since `graphql-js` version `15.0`, it's also possible for interface type to [implement other interface types](https://github.com/graphql/graphql-js/pull/2084). + +To accomplish this, we can just use the same syntax that we utilize for object types - the `implements` decorator option: + +```ts +@InterfaceType() +class Node { + @Field(type => ID) + id: string; +} + +@InterfaceType({ implements: Node }) +class Person extends Node { + @Field() + name: string; + + @Field(type => Int) + age: number; +} +``` + +Also, when we implement the interface that already implements other interface, there's no need to put them all in `implements` array in `@ObjectType` decorator option - only the closest one in the inheritance chain is required, e.g.: + +```ts +@ObjectType({ implements: [Person] }) +class Student extends Person { + @Field() + universityName: string; +} +``` + +This example produces following representation in GraphQL SDL: + +```graphql +interface Node { + id: ID! +} + +interface Person implements Node { + id: ID! + name: String! + age: Int! +} + +type Student implements Node & Person { + id: ID! + name: String! + age: Int! + universityName: String! +} +``` + +## Resolvers and arguments + +What's more, we can define resolvers for the interface fields, using the same syntax we would use when defining one for our object type: + +```ts +@InterfaceType() +abstract class IPerson { + @Field() + firstName: string; + + @Field() + lastName: string; + + @Field() + fullName(): string { + return `${this.firstName} ${this.lastName}`; + } +} +``` + +They're inherited by all the object types that implements this interface type but does not provide their own resolver implementation for those fields. + +Additionally, if we want to declare that the interface accepts some arguments, e.g.: + +```graphql +interface IPerson { + avatar(size: Int!): String! +} +``` + +We can just use `@Arg` or `@Args` decorators as usual: + +```ts +@InterfaceType() +abstract class IPerson { + @Field() + avatar(@Arg("size") size: number): string { + return `http://i.pravatar.cc/${size}`; + } +} +``` + +Unfortunately, TypeScript doesn't allow using decorators on abstract methods. +So if we don't want to provide implementation for that field resolver, only to enforce some signature (args and return type), we have to throw an error inside the body: + +```ts +@InterfaceType() +abstract class IPerson { + @Field() + avatar(@Arg("size") size: number): string { + throw new Error("Method not implemented!"); + } +} +``` + +And then we need to extend the interface class and override the method by providing its body - it is required for all object types that implements that interface type: + +```ts +@ObjectType({ implements: IPerson }) +class Person extends IPerson { + avatar(size: number): string { + return `http://i.pravatar.cc/${size}`; + } +} +``` + +In order to extend the signature by providing additional arguments (like `format`), we need to redeclare the whole field signature: + +```ts +@ObjectType({ implements: IPerson }) +class Person implements IPerson { + @Field() + avatar(@Arg("size") size: number, @Arg("format") format: string): string { + return `http://i.pravatar.cc/${size}.${format}`; + } +} +``` + +Resolvers for interface type fields can be also defined on resolvers classes level, by using the `@FieldResolver` decorator: + +```ts +@Resolver(of => IPerson) +class IPersonResolver { + @FieldResolver() + avatar(@Root() person: IPerson, @Arg("size") size: number): string { + return `http://typegraphql.com/${person.id}/${size}`; + } +} +``` + +## Registering in schema + +By default, if the interface type is explicitly used in schema definition (used as a return type of a query/mutation or as some field type), all object types that implement that interface will be emitted in schema, so we don't need to do anything. + +However, in some cases like the `Node` interface that is used in Relay-based systems, this behavior might be not intended when exposing multiple, separates schemas (like a public and the private ones). + +In this situation, we can provide an `{ autoRegisterImplementations: false }` option to the `@InterfaceType` decorator to prevent emitting all this object types in the schema: + +```ts +@InterfaceType({ autoRegisterImplementations: false }) +abstract class Node { + @Field(type => ID) + id: string; +} +``` + +Then we need to add all the object types (that implement this interface type and which we want to expose in selected schema) to the `orphanedTypes` array option in `buildSchema`: + +```ts +const schema = await buildSchema({ + resolvers, + // Provide orphaned object types + orphanedTypes: [Person, Animal, Recipe], +}); +``` + +Be aware that if the object type class is explicitly used as the GraphQL type (like `Recipe` type as the return type of `addRecipe` mutation), it will be emitted regardless the `orphanedTypes` setting. + +## Resolving Type + +Be aware that when our object type is implementing a GraphQL interface type, **we have to return an instance of the type class** in our resolvers. Otherwise, `graphql-js` will not be able to detect the underlying GraphQL type correctly. + +We can also provide our own `resolveType` function implementation to the `@InterfaceType` options. This way we can return plain objects in resolvers and then determine the returned object type by checking the shape of the data object, the same ways [like in unions](./unions.md), e.g.: + +```ts +@InterfaceType({ + resolveType: value => { + if ("grades" in value) { + return "Student"; // Schema name of type string + } + return Person; // Or object type class + }, +}) +abstract class IPerson { + // ... +} +``` + +However in case of interfaces, it might be a little bit more tricky than with unions, as we might not remember all the object types that implements this particular interface. + +## Examples + +For more advanced usage examples of interfaces (and type inheritance), e.g. with query returning an interface type, go to [this examples folder](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/interfaces-inheritance). diff --git a/website/versioned_docs/version-2.0.0-rc.2/middlewares.md b/website/versioned_docs/version-2.0.0-rc.2/middlewares.md new file mode 100644 index 000000000..cbc7a598d --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/middlewares.md @@ -0,0 +1,209 @@ +--- +title: Middleware and guards +id: version-2.0.0-rc.2-middlewares +original_id: middlewares +--- + +Middleware are pieces of reusable code that can be easily attached to resolvers and fields. By using middleware we can extract the commonly used code from our resolvers and then declaratively attach it using a decorator or even registering it globally. + +## Creating Middleware + +### What is Middleware? + +Middleware is a very powerful but somewhat complicated feature. Basically, middleware is a function that takes 2 arguments: + +- resolver data - the same as resolvers (`root`, `args`, `context`, `info`) +- the `next` function - used to control the execution of the next middleware and the resolver to which it is attached + +We may be familiar with how middleware works in [`express.js`](https://expressjs.com/en/guide/writing-middleware.html) but TypeGraphQL middleware is inspired by [`koa.js`](http://koajs.com/#application). The difference is that the `next` function returns a promise of the value of subsequent middleware and resolver execution from the stack. + +This makes it easy to perform actions before or after resolver execution. So things like measuring execution time are simple to implement: + +```ts +export const ResolveTime: MiddlewareFn = async ({ info }, next) => { + const start = Date.now(); + await next(); + const resolveTime = Date.now() - start; + console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`); +}; +``` + +### Intercepting the execution result + +Middleware also has the ability to intercept the result of a resolver's execution. It's not only able to e.g. create a log but also replace the result with a new value: + +```ts +export const CompetitorInterceptor: MiddlewareFn = async (_, next) => { + const result = await next(); + if (result === "typegql") { + return "type-graphql"; + } + return result; +}; +``` + +It might not seem very useful from the perspective of this library's users but this feature was mainly introduced for plugin systems and 3rd-party library integration. Thanks to this, it's possible to e.g. wrap the returned object with a lazy-relation wrapper that automatically fetches relations from a database on demand under the hood. + +### Simple Middleware + +If we only want to do something before an action, like log the access to the resolver, we can just place the `return next()` statement at the end of our middleware: + +```ts +const LogAccess: MiddlewareFn = ({ context, info }, next) => { + const username: string = context.username || "guest"; + console.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); + return next(); +}; +``` + +### Guards + +Middleware can also break the middleware stack by not calling the `next` function. This way, the result returned from the middleware will be used instead of calling the resolver and returning it's result. + +We can also throw an error in the middleware if the execution must be terminated and an error returned to the user, e.g. when resolver arguments are incorrect. + +This way we can create a guard that blocks access to the resolver and prevents execution or any data return. + +```ts +export const CompetitorDetector: MiddlewareFn = async ({ args }, next) => { + if (args.frameworkName === "type-graphql") { + return "TypeGraphQL"; + } + if (args.frameworkName === "typegql") { + throw new Error("Competitive framework detected!"); + } + return next(); +}; +``` + +### Reusable Middleware + +Sometimes middleware has to be configurable, just like we pass a `roles` array to the [`@Authorized()` decorator](./authorization.md). In this case, we should create a simple middleware factory - a function that takes our configuration as a parameter and returns a middleware that uses the provided value. + +```ts +export function NumberInterceptor(minValue: number): MiddlewareFn { + return async (_, next) => { + const result = await next(); + // Hide values below minValue + if (typeof result === "number" && result < minValue) { + return null; + } + return result; + }; +} +``` + +Remember to call this middleware with an argument, e.g. `NumberInterceptor(3.0)`, when attaching it to a resolver! + +### Error Interceptors + +Middleware can also catch errors that were thrown during execution. This way, they can easily be logged and even filtered for info that can't be returned to the user: + +```ts +export const ErrorInterceptor: MiddlewareFn = async ({ context, info }, next) => { + try { + return await next(); + } catch (err) { + // Write error to file log + fileLog.write(err, context, info); + + // Hide errors from db like printing sql query + if (someCondition(err)) { + throw new Error("Unknown error occurred!"); + } + + // Rethrow the error + throw err; + } +}; +``` + +### Class-based Middleware + +Sometimes our middleware logic can be a bit complicated - it may communicate with a database, write logs to file, etc., so we might want to test it. In that case we create class middleware that is able to benefit from [dependency injection](./dependency-injection.md) and easily mock a file logger or a database repository. + +To accomplish this, we implement a `MiddlewareInterface`. Our class must have the `use` method that conforms with the `MiddlewareFn` signature. Below we can see how the previously defined `LogAccess` middleware looks after the transformation: + +```ts +export class LogAccess implements MiddlewareInterface { + constructor(private readonly logger: Logger) {} + + async use({ context, info }: ResolverData, next: NextFn) { + const username: string = context.username || "guest"; + this.logger.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); + return next(); + } +} +``` + +## How to use + +### Attaching Middleware + +To attach middleware to a resolver method, place the `@UseMiddleware()` decorator above the method declaration. It accepts an array of middleware that will be called in the provided order. We can also pass them without an array as it supports [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters): + +```ts +@Resolver() +export class RecipeResolver { + @Query() + @UseMiddleware(ResolveTime, LogAccess) + randomValue(): number { + return Math.random(); + } +} +``` + +If we want to apply the middlewares to all the resolver's class methods, we can put the decorator on top of the class declaration: + +```ts +@UseMiddleware(ResolveTime, LogAccess) +@Resolver() +export class RecipeResolver { + @Query() + randomValue(): number { + return Math.random(); + } + + @Query() + constantValue(): number { + return 21.37; + } +} +``` + +> Be aware that resolver's class middlewares are executed first, before the method's ones. + +We can also attach the middleware to the `ObjectType` fields, the same way as with the [`@Authorized()` decorator](./authorization.md). + +```ts +@ObjectType() +export class Recipe { + @Field() + title: string; + + @Field(type => [Int]) + @UseMiddleware(LogAccess) + ratings: number[]; +} +``` + +### Global Middleware + +However, for common middlewares like measuring resolve time or catching errors, it might be annoying to place a `@UseMiddleware(ResolveTime)` decorator on every field, method or resolver class. + +Hence, in TypeGraphQL we can also register a global middleware that will be called for each query, mutation, subscription and a field. For this, we use the `globalMiddlewares` property of the `buildSchema` configuration object: + +```ts +const schema = await buildSchema({ + resolvers: [RecipeResolver], + globalMiddlewares: [ErrorInterceptor, ResolveTime], +}); +``` + +### Custom Decorators + +If we want to use middlewares with a more descriptive and declarative API, we can also create a custom method decorators. See how to do this in [custom decorators docs](./custom-decorators.md#method-decorators). + +## Example + +See how different kinds of middlewares work in the [middlewares and custom decorators example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/middlewares-custom-decorators). diff --git a/website/versioned_docs/version-2.0.0-rc.2/resolvers.md b/website/versioned_docs/version-2.0.0-rc.2/resolvers.md new file mode 100644 index 000000000..a60dd8801 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/resolvers.md @@ -0,0 +1,352 @@ +--- +title: Resolvers +id: version-2.0.0-rc.2-resolvers +original_id: resolvers +--- + +Besides [declaring GraphQL's object types](./types-and-fields.md), TypeGraphQL allows us to easily create queries, mutations and field resolvers - like normal class methods, similar to REST controllers in frameworks like Java `Spring`, .NET `Web API` or TypeScript [`routing-controllers`](https://github.com/typestack/routing-controllers). + +## Queries and Mutations + +### Resolver classes + +First we create the resolver class and annotate it with the `@Resolver()` decorator. This class will behave like a controller from classic REST frameworks: + +```ts +@Resolver() +class RecipeResolver {} +``` + +We can use a DI framework (as described in the [dependency injection docs](./dependency-injection.md)) to inject class dependencies (like services or repositories) or to store data inside the resolver class - it's guaranteed to be a single instance per app. + +```ts +@Resolver() +class RecipeResolver { + private recipesCollection: Recipe[] = []; +} +``` + +Then we can create class methods which will handle queries and mutations. For example, let's add the `recipes` query to return a collection of all recipes: + +```ts +@Resolver() +class RecipeResolver { + private recipesCollection: Recipe[] = []; + + async recipes() { + // Fake async + return await this.recipesCollection; + } +} +``` + +We also need to do two things. +The first is to add the `@Query` decorator, which marks the class method as a GraphQL query. +The second is to provide the return type. Since the method is async, the reflection metadata system shows the return type as a `Promise`, so we have to add the decorator's parameter as `returns => [Recipe]` to declare it resolves to an array of `Recipe` object types. + +```ts +@Resolver() +class RecipeResolver { + private recipesCollection: Recipe[] = []; + + @Query(returns => [Recipe]) + async recipes() { + return await this.recipesCollection; + } +} +``` + +### Arguments + +Usually, queries have some arguments - it might be the id of a resource, a search phrase or pagination settings. TypeGraphQL allows you to define arguments in two ways. + +First is the inline method using the `@Arg()` decorator. The drawback is the need to repeating the argument name (due to a limitation of the reflection system) in the decorator parameter. As we can see below, we can also pass a `defaultValue` option that will be reflected in the GraphQL schema. + +```ts +@Resolver() +class RecipeResolver { + // ... + @Query(returns => [Recipe]) + async recipes( + @Arg("servings", { defaultValue: 2 }) servings: number, + @Arg("title", { nullable: true }) title?: string, + ): Promise { + // ... + } +} +``` + +This works well when there are 2 - 3 args. But when you have many more, the resolver's method definitions become bloated. In this case we can use a class definition to describe the arguments. It looks like the object type class but it has the `@ArgsType()` decorator on top. + +```ts +@ArgsType() +class GetRecipesArgs { + @Field(type => Int, { nullable: true }) + skip?: number; + + @Field(type => Int, { nullable: true }) + take?: number; + + @Field({ nullable: true }) + title?: string; +} +``` + +We can define default values for optional fields in the `@Field()` decorator using the `defaultValue` option or by using a property initializer - in both cases TypeGraphQL will reflect this in the schema by setting the default value, so users will be able to omit those args while sending a query. + +> Be aware that `defaultValue` works only for input args and fields, like `@Arg`, `@ArgsType` and `@InputType`. +> Setting `defaultValue` does not affect `@ObjectType` or `@InterfaceType` fields as they are for output purposes only. + +Also, this way of declaring arguments allows you to perform validation. You can find more details about this feature in the [validation docs](./validation.md). + +We can also define helper fields and methods for our args or input classes. But be aware that **defining constructors is strictly forbidden** and we shouldn't use them there, as TypeGraphQL creates instances of args and input classes under the hood by itself. + +```ts +import { Min, Max } from "class-validator"; + +@ArgsType() +class GetRecipesArgs { + @Field(type => Int, { defaultValue: 0 }) + @Min(0) + skip: number; + + @Field(type => Int) + @Min(1) + @Max(50) + take = 25; + + @Field({ nullable: true }) + title?: string; + + // Helpers - index calculations + get startIndex(): number { + return this.skip; + } + get endIndex(): number { + return this.skip + this.take; + } +} +``` + +Then all that is left to do is use the args class as the type of the method parameter. +We can use the destructuring syntax to gain access to single arguments as variables, instead of the reference to the whole args object. + +```ts +@Resolver() +class RecipeResolver { + // ... + @Query(returns => [Recipe]) + async recipes(@Args() { title, startIndex, endIndex }: GetRecipesArgs) { + // Example implementation + let recipes = this.recipesCollection; + if (title) { + recipes = recipes.filter(recipe => recipe.title === title); + } + return recipes.slice(startIndex, endIndex); + } +} +``` + +This declaration will result in the following part of the schema in SDL: + +```graphql +type Query { + recipes(skip: Int = 0, take: Int = 25, title: String): [Recipe!] +} +``` + +### Input types + +GraphQL mutations can be similarly created: Declare the class method, use the `@Mutation` decorator, create arguments, provide a return type (if needed) etc. But for mutations we usually use `input` types, hence TypeGraphQL allows us to create inputs in the same way as [object types](./types-and-fields.md) but by using the `@InputType()` decorator: + +```ts +@InputType() +class AddRecipeInput {} +``` + +To ensure we don't accidentally change the property type we leverage the TypeScript type checking system by implementing the `Partial` type: + +```ts +@InputType() +class AddRecipeInput implements Partial {} +``` + +We then declare any input fields we need, using the `@Field()` decorator: + +```ts +@InputType({ description: "New recipe data" }) +class AddRecipeInput implements Partial { + @Field() + title: string; + + @Field({ nullable: true }) + description?: string; +} +``` + +After that we can use the `AddRecipeInput` type in our mutation. We can do this inline (using the `@Arg()` decorator) or as a field of the args class like in the query example above. + +We may also need access to the context. To achieve this we use the `@Ctx()` decorator with the optional user-defined `Context` interface: + +```ts +@Resolver() +class RecipeResolver { + // ... + @Mutation() + addRecipe(@Arg("data") newRecipeData: AddRecipeInput, @Ctx() ctx: Context): Recipe { + // Example implementation + const recipe = RecipesUtils.create(newRecipeData, ctx.user); + this.recipesCollection.push(recipe); + return recipe; + } +} +``` + +Because our method is synchronous and explicitly returns `Recipe`, we can omit the `@Mutation()` type annotation. + +This declaration will result in the following part of the schema in SDL: + +```graphql +input AddRecipeInput { + title: String! + description: String +} +``` + +```graphql +type Mutation { + addRecipe(data: AddRecipeInput!): Recipe! +} +``` + +By using parameter decorators, we can get rid of unnecessary parameters (like `root`) that bloat our method definition and have to be ignored by prefixing the parameter name with `_`. Also, we can achieve a clean separation between GraphQL and our business code by using decorators, so our resolvers and their methods behave just like services which can be easily unit-tested. + +## Field resolvers + +Queries and mutations are not the only type of resolvers. We often create object type field resolvers (e.g. when a `user` type has a `posts` field) which we have to resolve by fetching relational data from the database. + +Field resolvers in TypeGraphQL are very similar to queries and mutations - we create them as a method on the resolver class but with a few modifications. First we declare which object type fields we are resolving by providing the type to the `@Resolver` decorator: + +```ts +@Resolver(of => Recipe) +class RecipeResolver { + // Queries and mutations +} +``` + +Then we create a class method that will become the field resolver. +In our example we have the `averageRating` field in the `Recipe` object type that should calculate the average from the `ratings` array. + +```ts +@Resolver(of => Recipe) +class RecipeResolver { + // Queries and mutations + + averageRating(recipe: Recipe) { + // ... + } +} +``` + +We then mark the method as a field resolver with the `@FieldResolver()` decorator. Since we've already defined the field type in the `Recipe` class definition, there's no need to redefine it. We also decorate the method parameters with the `@Root` decorator in order to inject the recipe object. + +```ts +@Resolver(of => Recipe) +class RecipeResolver { + // Queries and mutations + + @FieldResolver() + averageRating(@Root() recipe: Recipe) { + // ... + } +} +``` + +For enhanced type safety we can implement the `ResolverInterface` interface. +It's a small helper that checks if the return type of the field resolver methods, like `averageRating(...)`, matches the `averageRating` property of the `Recipe` class and whether the first parameter of the method is the actual object type (`Recipe` class). + +```ts +@Resolver(of => Recipe) +class RecipeResolver implements ResolverInterface { + // Queries and mutations + + @FieldResolver() + averageRating(@Root() recipe: Recipe) { + // ... + } +} +``` + +Here is the full implementation of the sample `averageRating` field resolver: + +```ts +@Resolver(of => Recipe) +class RecipeResolver implements ResolverInterface { + // Queries and mutations + + @FieldResolver() + averageRating(@Root() recipe: Recipe) { + const ratingsSum = recipe.ratings.reduce((a, b) => a + b, 0); + return recipe.ratings.length ? ratingsSum / recipe.ratings.length : null; + } +} +``` + +For simple resolvers like `averageRating` or deprecated fields that behave like aliases, you can create field resolvers inline in the object type class definition: + +```ts +@ObjectType() +class Recipe { + @Field() + title: string; + + @Field({ deprecationReason: "Use `title` instead" }) + get name(): string { + return this.title; + } + + @Field(type => [Rate]) + ratings: Rate[]; + + @Field(type => Float, { nullable: true }) + averageRating(@Arg("since") sinceDate: Date): number | null { + const ratings = this.ratings.filter(rate => rate.date > sinceDate); + if (!ratings.length) return null; + + const ratingsSum = ratings.reduce((a, b) => a + b, 0); + return ratingsSum / ratings.length; + } +} +``` + +However, if the code is more complicated and has side effects (i.e. api calls, fetching data from a databases), a resolver class method should be used instead. This way we can leverage the dependency injection mechanism, which is really helpful in testing. For example: + +```ts +import { Repository } from "typeorm"; + +@Resolver(of => Recipe) +class RecipeResolver implements ResolverInterface { + constructor( + // Dependency injection + private readonly userRepository: Repository, + ) {} + + @FieldResolver() + async author(@Root() recipe: Recipe) { + const author = await this.userRepository.findById(recipe.userId); + if (!author) throw new SomethingWentWrongError(); + return author; + } +} +``` + +Note that if a field name of a field resolver doesn't exist in the resolver object type, it will create a field in the schema with this name. This feature is useful when the field is purely calculable (eg. `averageRating` from `ratings` array) and to avoid polluting the class signature. + +## Resolver Inheritance + +Resolver class `inheritance` is an advanced topic covered in the [resolver inheritance docs](./inheritance.md#resolvers-inheritance). + +## Examples + +These code samples are just made up for tutorial purposes. +You can find more advanced, real examples in the [examples folder on the repository](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples). diff --git a/website/versioned_docs/version-2.0.0-rc.2/subscriptions.md b/website/versioned_docs/version-2.0.0-rc.2/subscriptions.md new file mode 100644 index 000000000..c9b0787cd --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/subscriptions.md @@ -0,0 +1,213 @@ +--- +title: Subscriptions +id: version-2.0.0-rc.2-subscriptions +original_id: subscriptions +--- + +GraphQL can be used to perform reads with queries and writes with mutations. +However, oftentimes clients want to get updates pushed to them from the server when data they care about changes. +To support that, GraphQL has a third operation: subscription. TypeGraphQL of course has great support for subscription, using the [`@graphql-yoga/subscriptions`](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions) package created by [`The Guild`](https://the-guild.dev/). + +## Creating Subscriptions + +Subscription resolvers are similar to [queries and mutation resolvers](./resolvers.md) but slightly more complicated. + +First we create a normal class method as always, but this time annotated with the `@Subscription()` decorator. + +```ts +class SampleResolver { + // ... + @Subscription() + newNotification(): Notification { + // ... + } +} +``` + +Then we have to provide the topics we wish to subscribe to. This can be a single topic string, an array of topics or a function to dynamically create a topic based on subscription arguments passed to the query. We can also use TypeScript enums for enhanced type safety. + +```ts +class SampleResolver { + // ... + @Subscription({ + topics: "NOTIFICATIONS", // Single topic + topics: ["NOTIFICATIONS", "ERRORS"] // Or topics array + topics: ({ args, context }) => args.topic // Or dynamic topic function + }) + newNotification(): Notification { + // ... + } +} +``` + +We can also provide the `filter` option to decide which topic events should trigger our subscription. +This function should return a `boolean` or `Promise` type. + +```ts +class SampleResolver { + // ... + @Subscription({ + topics: "NOTIFICATIONS", + filter: ({ payload, args }) => args.priorities.includes(payload.priority), + }) + newNotification(): Notification { + // ... + } +} +``` + +We can also provide a custom subscription logic which might be useful, e.g. if we want to use the Prisma subscription functionality or something similar. + +All we need to do is to use the `subscribe` option which should be a function that returns an `AsyncIterable` or a `Promise`. Example using Prisma 1 subscription feature: + +```ts +class SampleResolver { + // ... + @Subscription({ + subscribe: ({ root, args, context, info }) => { + return context.prisma.$subscribe.users({ mutation_in: [args.mutationType] }); + }, + }) + newNotification(): Notification { + // ... + } +} +``` + +> Be aware that we can't mix the `subscribe` option with the `topics` and `filter` options. If the filtering is still needed, we can use the [`filter` and `map` helpers](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions#filter-and-map-values) from the `@graphql-yoga/subscriptions` package. + +Now we can implement the subscription resolver. It will receive the payload from a triggered topic of the pubsub system using the `@Root()` decorator. There, we can transform it to the returned shape. + +```ts +class SampleResolver { + // ... + @Subscription({ + topics: "NOTIFICATIONS", + filter: ({ payload, args }) => args.priorities.includes(payload.priority), + }) + newNotification( + @Root() notificationPayload: NotificationPayload, + @Args() args: NewNotificationsArgs, + ): Notification { + return { + ...notificationPayload, + date: new Date(), + }; + } +} +``` + +## Triggering subscription topics + +Ok, we've created subscriptions, but what is the `pubsub` system and how do we trigger topics? + +They might be triggered from external sources like a database but also in mutations, +e.g. when we modify some resource that clients want to receive notifications about when it changes. + +So, let us assume we have this mutation for adding a new comment: + +```ts +class SampleResolver { + // ... + @Mutation(returns => Boolean) + async addNewComment(@Arg("comment") input: CommentInput) { + const comment = this.commentsService.createNew(input); + await this.commentsRepository.save(comment); + return true; + } +} +``` + +First, we need to create the `PubSub` instance. In most cases, we call `createPubSub()` function from `@graphql-yoga/subscriptions` package. Optionally, we can define the used topics and payload type using the type argument, e.g.: + +```ts +import { createPubSub } from "@graphql-yoga/subscriptions"; + +export const pubSub = createPubSub<{ + NOTIFICATIONS: [NotificationPayload]; + DYNAMIC_ID_TOPIC: [number, NotificationPayload]; +}>(); +``` + +Then, we need to register the `PubSub` instance in the `buildSchema()` function options: + +```ts +import { buildSchema } from "type-graphql"; +import { pubSub } from "./pubsub"; + +const schema = await buildSchema({ + resolver, + pubSub, +}); +``` + +Finally, we can use the created `PubSub` instance to trigger the topics and send the payload to all topic subscribers: + +```ts +import { pubSub } from "./pubsub"; + +class SampleResolver { + // ... + @Mutation(returns => Boolean) + async addNewComment(@Arg("comment") input: CommentInput, @PubSub() pubSub: PubSubEngine) { + const comment = this.commentsService.createNew(input); + await this.commentsRepository.save(comment); + // Trigger subscriptions topics + const payload: NotificationPayload = { message: input.content }; + pubSub.publish("NOTIFICATIONS", payload); + return true; + } +} +``` + +And that's it! Now all subscriptions attached to the `NOTIFICATIONS` topic will be triggered when performing the `addNewComment` mutation. + +## Topic with dynamic ID + +The idea of this feature is taken from the `@graphql-yoga/subscriptions` that is used under the hood. +Basically, sometimes you only want to emit and listen for events for a specific entity (e.g. user or product). Dynamic topic ID lets you declare topics scoped to a special identifier, e.g.: + +```ts +@Resolver() +class NotificationResolver { + @Subscription({ + topics: "NOTIFICATIONS", + topicId: ({ context }) => context.userId, + }) + newNotification(@Root() { message }: NotificationPayload): Notification { + return { message, date: new Date() }; + } +} +``` + +Then in your mutation or services, you need to pass the topic id as the second parameter: + +```ts +pubSub.publish("NOTIFICATIONS", userId, { id, message }); +``` + +> Be aware that this feature must be supported by the pubsub system of your choice. +> If you decide to use something different than `createPubSub()` from `@graphql-yoga/subscriptions`, the second argument might be treated as a payload, not dynamic topic id. + +## Using a custom PubSub system + +While TypeGraphQL uses the `@graphql-yoga/subscriptions` package under the hood to handle subscription, there's no requirement to use that implementation of `PubSub`. + +In fact, you can use any pubsub system you want, not only the `graphql-yoga` one. +The only requirement is to comply with the exported `PubSub` interface - having proper `.subscribe()` and `.publish()` methods. + +This is especially helpful for production usage, where we can't rely on the in-memory event emitter, so that we [use distributed pubsub](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions#distributed-pubsub-for-production). + +## Creating a Subscription Server + +The [bootstrap guide](./bootstrap.md) and all the earlier examples used [`apollo-server`](https://github.com/apollographql/apollo-server) to create an HTTP endpoint for our GraphQL API. + +However, beginning in Apollo Server 3, subscriptions are not supported by the "batteries-included" apollo-server package. To enable subscriptions, you need to follow the guide on their docs page: + + +## Examples + +See how subscriptions work in a [simple example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/simple-subscriptions). You can see there, how simple is setting up GraphQL subscriptions using `graphql-yoga` package. + +For production usage, it's better to use something more scalable like a Redis-based pubsub system - [a working example is also available](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/redis-subscriptions). +However, to launch this example you need to have a running instance of Redis and you might have to modify the example code to provide your connection parameters. diff --git a/website/versioned_docs/version-2.0.0-rc.2/unions.md b/website/versioned_docs/version-2.0.0-rc.2/unions.md new file mode 100644 index 000000000..d33b6af68 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/unions.md @@ -0,0 +1,109 @@ +--- +title: Unions +id: version-2.0.0-rc.2-unions +original_id: unions +--- + +Sometimes our API has to be flexible and return a type that is not specific but one from a range of possible types. An example might be a movie site's search functionality: using the provided phrase we search the database for movies but also actors. So the query has to return a list of `Movie` or `Actor` types. + +Read more about the GraphQL Union Type in the [official GraphQL docs](http://graphql.org/learn/schema/#union-types). + +## Usage + +Let's start by creating the object types from the example above: + +```ts +@ObjectType() +class Movie { + @Field() + name: string; + + @Field() + rating: number; +} +``` + +```ts +@ObjectType() +class Actor { + @Field() + name: string; + + @Field(type => Int) + age: number; +} +``` + +Now let's create an union type from the object types above - the rarely seen `[ ] as const` syntax is to inform TypeScript compiler that it's a tuple, which allows for better TS union type inference: + +```ts +import { createUnionType } from "type-graphql"; + +const SearchResultUnion = createUnionType({ + name: "SearchResult", // Name of the GraphQL union + types: () => [Movie, Actor] as const, // function that returns tuple of object types classes +}); +``` + +Then we can use the union type in the query by providing the `SearchResultUnion` value in the `@Query` decorator return type annotation. +Notice, that we have to explicitly use the decorator return type annotation due to TypeScript's reflection limitations. +For TypeScript compile-time type safety we can also use `typeof SearchResultUnion` which is equal to type `Movie | Actor`. + +```ts +@Resolver() +class SearchResolver { + @Query(returns => [SearchResultUnion]) + async search(@Arg("phrase") phrase: string): Promise> { + const movies = await Movies.findAll(phrase); + const actors = await Actors.findAll(phrase); + + return [...movies, ...actors]; + } +} +``` + +## Resolving Type + +Be aware that when the query/mutation return type (or field type) is a union, we have to return a specific instance of the object type class. Otherwise, `graphql-js` will not be able to detect the underlying GraphQL type correctly when we use plain JS objects. + +However, we can also provide our own `resolveType` function implementation to the `createUnionType` options. This way we can return plain objects in resolvers and then determine the returned object type by checking the shape of the data object, e.g.: + +```ts +const SearchResultUnion = createUnionType({ + name: "SearchResult", + types: () => [Movie, Actor] as const, + // Implementation of detecting returned object type + resolveType: value => { + if ("rating" in value) { + return Movie; // Return object type class (the one with `@ObjectType()`) + } + if ("age" in value) { + return "Actor"; // Or the schema name of the type as a string + } + return undefined; + }, +}); +``` + +**Et VoilΓ !** We can now build the schema and make the example query πŸ˜‰ + +```graphql +query { + search(phrase: "Holmes") { + ... on Actor { + # Maybe Katie Holmes? + name + age + } + ... on Movie { + # For sure Sherlock Holmes! + name + rating + } + } +} +``` + +## Examples + +More advanced usage examples of unions (and enums) are located in [this examples folder](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/enums-and-unions). diff --git a/website/versioned_docs/version-2.0.0-rc.2/validation.md b/website/versioned_docs/version-2.0.0-rc.2/validation.md new file mode 100644 index 000000000..b5f33870b --- /dev/null +++ b/website/versioned_docs/version-2.0.0-rc.2/validation.md @@ -0,0 +1,258 @@ +--- +title: Argument and Input validation +sidebar_label: Validation +id: version-2.0.0-rc.2-validation +original_id: validation +--- + +## Scalars + +The standard way to ensure that inputs and arguments are correct, such as an `email` field that really contains a proper e-mail address, is to use [custom scalars](./scalars.md) e.g. `GraphQLEmail` from [`graphql-custom-types`](https://github.com/stylesuxx/graphql-custom-types). However, creating scalars for all single cases of data types (credit card number, base64, IP, URL) might be cumbersome. + +That's why TypeGraphQL has built-in support for argument and input validation. +By default, we can use the [`class-validator`](https://github.com/typestack/class-validator) library and easily declare the requirements for incoming data (e.g. a number is in the range 0-255 or a password that is longer than 8 characters) thanks to the awesomeness of decorators. + +We can also use other libraries or our own custom solution, as described in [custom validators](#custom-validator) section. + +## `class-validator` + +### How to use + +First, we need to install the `class-validator` package: + +```sh +npm install class-validator +``` + +Then we decorate the input/arguments class with the appropriate decorators from `class-validator`. +So we take this: + +```ts +@InputType() +export class RecipeInput { + @Field() + title: string; + + @Field({ nullable: true }) + description?: string; +} +``` + +...and turn it into this: + +```ts +import { MaxLength, Length } from "class-validator"; + +@InputType() +export class RecipeInput { + @Field() + @MaxLength(30) + title: string; + + @Field({ nullable: true }) + @Length(30, 255) + description?: string; +} +``` + +Then we need to enable the auto-validate feature (as it's disabled by default) by simply setting `validate: true` in `buildSchema` options, e.g.: + +```ts +const schema = await buildSchema({ + resolvers: [RecipeResolver], + validate: true, // Enable 'class-validator' integration +}); +``` + +And that's it! πŸ˜‰ + +TypeGraphQL will automatically validate our inputs and arguments based on the definitions: + +```ts +@Resolver(of => Recipe) +export class RecipeResolver { + @Mutation(returns => Recipe) + async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise { + // 100% sure that the input is correct + console.assert(recipeInput.title.length <= 30); + console.assert(recipeInput.description.length >= 30); + console.assert(recipeInput.description.length <= 255); + } +} +``` + +Of course, [there are many more decorators](https://github.com/typestack/class-validator#validation-decorators) we have access to, not just the simple `@Length` decorator used in the example above, so take a look at the `class-validator` documentation. + +This feature is enabled by default. However, we can disable it if we must: + +```ts +const schema = await buildSchema({ + resolvers: [RecipeResolver], + validate: false, // Disable automatic validation or pass the default config object +}); +``` + +And we can still enable it per resolver's argument if we need to: + +```ts +class RecipeResolver { + @Mutation(returns => Recipe) + async addRecipe(@Arg("input", { validate: true }) recipeInput: RecipeInput) { + // ... + } +} +``` + +The `ValidatorOptions` object used for setting features like [validation groups](https://github.com/typestack/class-validator#validation-groups) can also be passed: + +```ts +class RecipeResolver { + @Mutation(returns => Recipe) + async addRecipe( + @Arg("input", { validate: { groups: ["admin"] } }) + recipeInput: RecipeInput, + ) { + // ... + } +} +``` + +Note that by default, the `skipMissingProperties` setting of the `class-validator` is set to `true` because GraphQL will independently check whether the params/fields exist or not. +Same goes to `forbidUnknownValues` setting which is set to `false` because the GraphQL runtime checks for additional data, not described in schema. + +GraphQL will also check whether the fields have correct types (String, Int, Float, Boolean, etc.) so we don't have to use the `@IsOptional`, `@Allow`, `@IsString` or the `@IsInt` decorators at all! + +However, when using nested input or arrays, we always have to use [`@ValidateNested()` decorator](https://github.com/typestack/class-validator#validating-nested-objects) or [`{ each: true }` option](https://github.com/typestack/class-validator#validating-arrays) to make nested validation work properly. + +### Response to the Client + +When a client sends incorrect data to the server: + +```graphql +mutation ValidationMutation { + addRecipe( + input: { + # Too long! + title: "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet" + } + ) { + title + creationDate + } +} +``` + +the [`ArgumentValidationError`](https://github.com/MichalLytek/type-graphql/blob/master/src/errors/ArgumentValidationError.ts) will be thrown. + +By default, the `apollo-server` package from the [bootstrap guide](./bootstrap.md) will format the error to match the `GraphQLFormattedError` interface. So when the `ArgumentValidationError` occurs, the client will receive this JSON with a nice `validationErrors` property inside of `extensions.exception`: + +```json +{ + "errors": [ + { + "message": "Argument Validation Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": ["addRecipe"], + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exception": { + "validationErrors": [ + { + "target": { + "title": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet" + }, + "value": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet", + "property": "title", + "children": [], + "constraints": { + "maxLength": "title must be shorter than or equal to 30 characters" + } + } + ], + "stacktrace": [ + "Error: Argument Validation Error", + " at Object. (/type-graphql/src/resolvers/validate-arg.ts:29:11)", + " at Generator.throw ()", + " at rejected (/type-graphql/node_modules/tslib/tslib.js:105:69)", + " at processTicksAndRejections (internal/process/next_tick.js:81:5)" + ] + } + } + } + ], + "data": null +} +``` + +Of course we can also create our own custom implementation of the `formatError` function provided in the `ApolloServer` config options which will transform the `GraphQLError` with a `ValidationError` array in the desired output format (e.g. `extensions.code = "ARGUMENT_VALIDATION_ERROR"`). + +### Automatic Validation Example + +To see how this works, check out the [simple real life example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/automatic-validation). + +### Caveats + +Even if we don't use the validation feature (and we have provided `{ validate: false }` option to `buildSchema`), we still need to have `class-validator` installed as a dev dependency in order to compile our app without errors using `tsc`. + +An alternative solution that allows to completely get rid off big `class-validator` from our project's `node_modules` folder is to suppress the `error TS2307: Cannot find module 'class-validator'` TS error by providing `"skipLibCheck": true` setting in `tsconfig.json`. + +## Custom validator + +We can also use other libraries than `class-validator` together with TypeGraphQL. + +To integrate it, all we need to do is to provide a custom function. +It receives three parameters: + +- `argValue` which is the injected value of `@Arg()` or `@Args()` +- `argType` which is a runtime type information (e.g. `String` or `RecipeInput`) +- `resolverData` which holds the resolver execution context, described as generic type `ResolverData` + +This function can be an async function and should return nothing (`void`) when validation passes, or throw an error when validation fails. +So be aware of this while trying to wrap another library in `validateFn` function for TypeGraphQL. + +Then we provide this function as a `validateFn` option in `buildSchema`. +Example using [decorators library for Joi validators (`joiful`)](https://github.com/joiful-ts/joiful): + +```ts +const schema = await buildSchema({ + // ... + validateFn: argValue => { + // Call joiful validate + const { error } = joiful.validate(argValue); + if (error) { + // Throw error on failed validation + throw error; + } + }, +}); +``` + +The `validateFn` option is also supported as a `@Arg()` or `@Args()` decorator option, e.g.: + +```ts +@Resolver() +class SampleResolver { + @Query() + sampleQuery( + @Arg("sampleArg", { + validateFn: (argValue, argType) => { + // Do something here with arg value and type... + }, + }) + sampleArg: string, + ): string { + // ... + } +} +``` + +> Be aware that when using custom validator, the error won't be wrapped with `ArgumentValidationError` like for the built-in `class-validator` validation. + +### Custom Validation Example + +To see how this works, check out the [simple custom validation integration example](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-rc.2/examples/custom-validation). diff --git a/website/versioned_sidebars/version-2.0.0-rc.2-sidebars.json b/website/versioned_sidebars/version-2.0.0-rc.2-sidebars.json new file mode 100644 index 000000000..72b6126e1 --- /dev/null +++ b/website/versioned_sidebars/version-2.0.0-rc.2-sidebars.json @@ -0,0 +1,60 @@ +{ + "version-2.0.0-rc.2-docs": { + "Introduction": [ + "version-2.0.0-rc.2-introduction" + ], + "Beginner guides": [ + "version-2.0.0-rc.2-installation", + "version-2.0.0-rc.2-getting-started", + "version-2.0.0-rc.2-types-and-fields", + "version-2.0.0-rc.2-resolvers", + "version-2.0.0-rc.2-bootstrap", + "version-2.0.0-rc.2-esm" + ], + "Migration guide": [ + "version-2.0.0-rc.2-migration-guide" + ], + "Advanced guides": [ + "version-2.0.0-rc.2-scalars", + "version-2.0.0-rc.2-enums", + "version-2.0.0-rc.2-unions", + "version-2.0.0-rc.2-interfaces", + "version-2.0.0-rc.2-subscriptions", + "version-2.0.0-rc.2-directives", + "version-2.0.0-rc.2-extensions" + ], + "Features": [ + "version-2.0.0-rc.2-dependency-injection", + "version-2.0.0-rc.2-authorization", + "version-2.0.0-rc.2-validation", + "version-2.0.0-rc.2-inheritance", + "version-2.0.0-rc.2-generic-types", + "version-2.0.0-rc.2-middlewares", + "version-2.0.0-rc.2-custom-decorators", + "version-2.0.0-rc.2-complexity" + ], + "Integrations": [ + "version-2.0.0-rc.2-prisma", + "version-2.0.0-rc.2-nestjs" + ], + "Others": [ + "version-2.0.0-rc.2-emit-schema", + "version-2.0.0-rc.2-performance" + ], + "Recipes": [ + "version-2.0.0-rc.2-browser-usage", + "version-2.0.0-rc.2-aws-lambda", + "version-2.0.0-rc.2-azure-functions" + ] + }, + "version-2.0.0-rc.2-examples": { + "Examples": [ + "version-2.0.0-rc.2-examples" + ] + }, + "version-2.0.0-rc.2-others": { + "Others": [ + "version-2.0.0-rc.2-faq" + ] + } +} diff --git a/website/versions.json b/website/versions.json index 0eaf52812..eacab01d5 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "2.0.0-rc.2", "2.0.0-rc.1", "2.0.0-beta.6", "2.0.0-beta.4",