From f50d1d0af5b7b90d3d4b69c873188030f7740f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lytek?= Date: Thu, 17 Aug 2023 17:28:18 +0200 Subject: [PATCH] release: 2.0.0-beta.3 --- CHANGELOG.md | 4 +- package-lock.json | 4 +- package.json | 2 +- .../version-2.0.0-beta.3/authorization.md | 195 ++++++++++ .../version-2.0.0-beta.3/bootstrap.md | 143 +++++++ .../version-2.0.0-beta.3/browser-usage.md | 40 ++ .../version-2.0.0-beta.3/complexity.md | 103 +++++ .../version-2.0.0-beta.3/custom-decorators.md | 109 ++++++ .../dependency-injection.md | 180 +++++++++ .../version-2.0.0-beta.3/directives.md | 131 +++++++ .../version-2.0.0-beta.3/emit-schema.md | 66 ++++ .../version-2.0.0-beta.3/enums.md | 143 +++++++ .../version-2.0.0-beta.3/esm.md | 48 +++ .../version-2.0.0-beta.3/examples.md | 52 +++ .../version-2.0.0-beta.3/extensions.md | 119 ++++++ .../version-2.0.0-beta.3/faq.md | 93 +++++ .../version-2.0.0-beta.3/generic-types.md | 169 +++++++++ .../version-2.0.0-beta.3/getting-started.md | 190 ++++++++++ .../version-2.0.0-beta.3/inheritance.md | 145 ++++++++ .../version-2.0.0-beta.3/installation.md | 63 ++++ .../version-2.0.0-beta.3/interfaces.md | 258 +++++++++++++ .../version-2.0.0-beta.3/introduction.md | 59 +++ .../version-2.0.0-beta.3/middlewares.md | 189 ++++++++++ .../version-2.0.0-beta.3/nestjs.md | 49 +++ .../version-2.0.0-beta.3/performance.md | 83 +++++ .../version-2.0.0-beta.3/prisma.md | 54 +++ .../version-2.0.0-beta.3/resolvers.md | 352 ++++++++++++++++++ .../version-2.0.0-beta.3/scalars.md | 175 +++++++++ .../version-2.0.0-beta.3/subscriptions.md | 191 ++++++++++ .../version-2.0.0-beta.3/types-and-fields.md | 140 +++++++ .../version-2.0.0-beta.3/unions.md | 109 ++++++ .../version-2.0.0-beta.3/validation.md | 237 ++++++++++++ .../version-2.0.0-beta.3-sidebars.json | 53 +++ website/versions.json | 1 + 34 files changed, 3945 insertions(+), 4 deletions(-) create mode 100644 website/versioned_docs/version-2.0.0-beta.3/authorization.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/bootstrap.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/browser-usage.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/complexity.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/custom-decorators.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/dependency-injection.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/directives.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/emit-schema.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/enums.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/esm.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/examples.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/extensions.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/faq.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/generic-types.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/getting-started.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/inheritance.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/installation.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/interfaces.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/introduction.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/middlewares.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/nestjs.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/performance.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/prisma.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/resolvers.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/scalars.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/subscriptions.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/types-and-fields.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/unions.md create mode 100644 website/versioned_docs/version-2.0.0-beta.3/validation.md create mode 100644 website/versioned_sidebars/version-2.0.0-beta.3-sidebars.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b950e4d..189b86c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog and release notes -## Unreleased + +## v2.0.0-beta.3 + ### Features - **Breaking Change**: update `graphql-js` peer dependency to `^16.7.1` diff --git a/package-lock.json b/package-lock.json index 08f415912..0f588e814 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "type-graphql", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "type-graphql", - "version": "2.0.0", + "version": "2.0.0-beta.3", "funding": [ { "type": "github", diff --git a/package.json b/package.json index 59dc7b229..44a727413 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "type-graphql", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "private": false, "description": "Create GraphQL schema and resolvers with TypeScript, using classes and decorators!", "keywords": [ diff --git a/website/versioned_docs/version-2.0.0-beta.3/authorization.md b/website/versioned_docs/version-2.0.0-beta.3/authorization.md new file mode 100644 index 000000000..c72b0cb0d --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/authorization.md @@ -0,0 +1,195 @@ +--- +title: Authorization +id: version-2.0.0-beta.3-authorization +original_id: authorization +--- + +Authorization is a core feature used in almost all APIs. Sometimes we want to restrict data access or actions for a specific group of users. + +In express.js (and other Node.js frameworks) we use middleware for this, like `passport.js` or the custom ones. However, in GraphQL's resolver architecture we don't have middleware so we have to imperatively call the auth checking function and manually pass context data to each resolver, which might be a bit tedious. + +That's why authorization is a first-class feature in `TypeGraphQL`! + +## How to use + +First, we need to use the `@Authorized` decorator as a guard on a field, query or mutation. +Example object type field guards: + +```ts +@ObjectType() +class MyObject { + @Field() + publicField: string; + + @Authorized() + @Field() + authorizedField: string; + + @Authorized("ADMIN") + @Field() + adminField: string; + + @Authorized(["ADMIN", "MODERATOR"]) + @Field({ nullable: true }) + hiddenField?: string; +} +``` + +We can leave the `@Authorized` decorator brackets empty or we can specify the role/roles that the user needs to possess in order to get access to the field, query or mutation. +By default the roles are of type `string` but they can easily be changed as the decorator is generic - `@Authorized(1, 7, 22)`. + +Thus, authorized users (regardless of their roles) can only read the `publicField` or the `authorizedField` from the `MyObject` object. They will receive `null` when accessing the `hiddenField` field and will receive an error (that will propagate through the whole query tree looking for a nullable field) for the `adminField` when they don't satisfy the role constraints. + +Sample query and mutation guards: + +```ts +@Resolver() +class MyResolver { + @Query() + publicQuery(): MyObject { + return { + publicField: "Some public data", + authorizedField: "Data for logged users only", + adminField: "Top secret info for admin", + }; + } + + @Authorized() + @Query() + authedQuery(): string { + return "Authorized users only!"; + } + + @Authorized("ADMIN", "MODERATOR") + @Mutation() + adminMutation(): string { + return "You are an admin/moderator, you can safely drop the database ;)"; + } +} +``` + +Authorized users (regardless of their roles) will be able to read data from the `publicQuery` and the `authedQuery` queries, but will receive an error when trying to perform the `adminMutation` when their roles don't include `ADMIN` or `MODERATOR`. + +Next, we need to create our auth checker function. Its implementation may depend on our business logic: + +```ts +export const customAuthChecker: AuthChecker = ( + { 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-beta.3/examples/authorization). diff --git a/website/versioned_docs/version-2.0.0-beta.3/bootstrap.md b/website/versioned_docs/version-2.0.0-beta.3/bootstrap.md new file mode 100644 index 000000000..ce9f41d30 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/bootstrap.md @@ -0,0 +1,143 @@ +--- +title: Bootstrapping +id: version-2.0.0-beta.3-bootstrap +original_id: bootstrap +--- + +After creating our resolvers, type classes, and other business-related code, we need to make our app run. First we have to build the schema, then we can expose it with an HTTP server, WebSockets or even MQTT. + +## Create Executable Schema + +To create an executable schema from type and resolver definitions, we need to use the `buildSchema` function. +It takes a configuration object as a parameter and returns a promise of a `GraphQLSchema` object. + +In the configuration object we must provide a `resolvers` property, which is supposed to be an array of resolver classes: + +```ts +import { FirstResolver, SecondResolver } from "./resolvers"; +// ... +const schema = await buildSchema({ + resolvers: [FirstResolver, SecondResolver], +}); +``` + +Be aware that only operations (queries, mutation, etc.) defined in the resolvers classes (and types directly connected to them) will be emitted in schema. + +So if we have defined some object types (that implements an interface type [with disabled auto registering](./interfaces.md#registering-in-schema)) but are not directly used in other types definition (like a part of an union, a type of a field or a return type of an operation), we need to provide them manually in `orphanedTypes` options of `buildSchema`: + +```ts +import { FirstResolver, SecondResolver } from "../app/src/resolvers"; +import { FirstObject } from "../app/src/types"; +// ... +const schema = await buildSchema({ + resolvers: [FirstResolver, SecondResolver], + // Provide all the types that are missing in schema + orphanedTypes: [FirstObject], +}); +``` + +In case of defining the resolvers array somewhere else (not inline in the `buildSchema`), we need to use the `as const` syntax to inform the TS compiler and satisfy the `NonEmptyArray` constraints: + +```ts +// resolvers.ts +export const resolvers = [FirstResolver, SecondResolver] as const; + +// schema.ts +import { resolvers } from "./resolvers"; + +const schema = await buildSchema({ resolvers }); +``` + +There are also other options related to advanced features like [authorization](./authorization.md) or [validation](./validation.md) - you can read about them in docs. + +To make `await` work, we need to declare it as an async function. Example of `main.ts` file: + +```ts +import { buildSchema } from "type-graphql"; + +async function bootstrap() { + const schema = await buildSchema({ + resolvers: [ + // ... Resolvers classes + ], + }); + + // ... +} + +bootstrap(); // Actually run the async function +``` + +## Create an HTTP GraphQL endpoint + +In most cases, the GraphQL app is served by an HTTP server. After building the schema we can create the GraphQL endpoint with a variety of tools such as [`graphql-yoga`](https://github.com/dotansimha/graphql-yoga) or [`@apollo/server`](https://github.com/apollographql/apollo-server). + +Below is an example using [`@apollo/server`](https://github.com/apollographql/apollo-server): + +```ts +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; + +const PORT = process.env.PORT || 4000; + +async function bootstrap() { + // ... Build GraphQL schema + + // Create GraphQL server + const server = new ApolloServer({ schema }); + + // Start server + const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); + console.log(`GraphQL server ready at ${url}`); +} + +bootstrap(); +``` + +Remember to install the `@apollo/server` package from npm - it's not bundled with TypeGraphQL. + +Of course you can use the `express-graphql` middleware, `graphql-yoga` or whatever you want ๐Ÿ˜‰ + +## Create typeDefs and resolvers map + +TypeGraphQL provides a second way to generate the GraphQL schema - the `buildTypeDefsAndResolvers` function. + +It accepts the same `BuildSchemaOptions` as the `buildSchema` function but instead of an executable `GraphQLSchema`, it creates a typeDefs and resolversMap pair that you can use e.g. with [@graphql-tools/\*`](https://the-guild.dev/graphql/tools): + +```ts +import { makeExecutableSchema } from "@graphql-tools/schema"; + +const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ + resolvers: [FirstResolver, SecondResolver], +}); + +const schema = makeExecutableSchema({ typeDefs, resolvers }); +``` + +Or even with other libraries that expect the schema info in that shape, like [`apollo-link-state`](https://github.com/apollographql/apollo-link-state): + +```ts +import { withClientState } from "apollo-link-state"; + +const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ + resolvers: [FirstResolver, SecondResolver], +}); + +const stateLink = withClientState({ + // ... Other options like `cache` + typeDefs, + resolvers, +}); + +// ... Rest of `ApolloClient` initialization code +``` + +There's also a `sync` version of it - `buildTypeDefsAndResolversSync`: + +```ts +const { typeDefs, resolvers } = buildTypeDefsAndResolversSync({ + resolvers: [FirstResolver, SecondResolver], +}); +``` + +However, be aware that some of the TypeGraphQL features (i.a. [query complexity](./complexity.md)) might not work with the `buildTypeDefsAndResolvers` approach because they use some low-level `graphql-js` features. diff --git a/website/versioned_docs/version-2.0.0-beta.3/browser-usage.md b/website/versioned_docs/version-2.0.0-beta.3/browser-usage.md new file mode 100644 index 000000000..14fcffcf3 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/browser-usage.md @@ -0,0 +1,40 @@ +--- +title: Browser usage +id: version-2.0.0-beta.3-browser-usage +original_id: browser-usage +--- + +## Using classes in a client app + +Sometimes we might want to use the classes we've created and annotated with TypeGraphQL decorators, in our client app that works in the browser. For example, reusing the args or input classes with `class-validator` decorators or the object type classes with some helpful custom methods. + +Since TypeGraphQL is a Node.js framework, it doesn't work in a browser environment, so we may quickly get an error, e.g. `ERROR in ./node_modules/fs.realpath/index.js` or `utils1_promisify is not a function`, while trying to build our app with Webpack. To correct this, we have to configure Webpack to use the decorator shim instead of the normal module. We simply add this plugin code to our webpack config: + +```js +module.exports = { + // ... Rest of Webpack configuration + plugins: [ + // ... Other existing plugins + new webpack.NormalModuleReplacementPlugin(/type-graphql$/, resource => { + resource.request = resource.request.replace(/type-graphql/, "type-graphql/dist/browser-shim.js"); + }), + ]; +} +``` + +In case of cypress, you can adapt the same webpack config trick just by applying the [cypress-webpack-preprocessor](https://github.com/cypress-io/cypress-webpack-preprocessor) plugin. + +However, in some TypeScript projects like the ones using Angular, which AoT compiler requires that a full `*.ts` file is provided instead of just a `*.js` and `*.d.ts` files, to use this shim we have to simply set up our TypeScript configuration in `tsconfig.json` to use this file instead of a normal TypeGraphQL module: + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "type-graphql": ["./node_modules/type-graphql/dist/browser-shim.ts"] + } + } +} +``` + +Thanks to this, our bundle will be much lighter as we don't need to embed the whole TypeGraphQL library code in our app. diff --git a/website/versioned_docs/version-2.0.0-beta.3/complexity.md b/website/versioned_docs/version-2.0.0-beta.3/complexity.md new file mode 100644 index 000000000..d50e2f3c7 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/complexity.md @@ -0,0 +1,103 @@ +--- +title: Query complexity +id: version-2.0.0-beta.3-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-beta.3/examples/query-complexity). diff --git a/website/versioned_docs/version-2.0.0-beta.3/custom-decorators.md b/website/versioned_docs/version-2.0.0-beta.3/custom-decorators.md new file mode 100644 index 000000000..829eb22d5 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/custom-decorators.md @@ -0,0 +1,109 @@ +--- +title: Custom decorators +id: version-2.0.0-beta.3-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 two kinds of custom decorators - method 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 `createMethodDecorator` helper function with our middleware logic and return its value: + +```ts +export function ValidateArgs(schema: JoiSchema) { + return createMethodDecorator(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; + } +} +``` + +## 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 createParamDecorator(({ context }) => context.currentUser); +} +``` + +Or might be a more advanced one that performs some calculations and encapsulates some logic. Compared to middlewares, they allows 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 createParamDecorator(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, + }); + } +} +``` + +## 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-beta.3/examples/middlewares-custom-decorators). diff --git a/website/versioned_docs/version-2.0.0-beta.3/dependency-injection.md b/website/versioned_docs/version-2.0.0-beta.3/dependency-injection.md new file mode 100644 index 000000000..70638033d --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/dependency-injection.md @@ -0,0 +1,180 @@ +--- +title: Dependency injection +id: version-2.0.0-beta.3-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-beta.3/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-beta.3/examples/using-scoped-container). + +Integration with [TSyringe](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/tsyringe). diff --git a/website/versioned_docs/version-2.0.0-beta.3/directives.md b/website/versioned_docs/version-2.0.0-beta.3/directives.md new file mode 100644 index 000000000..c972e0843 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/directives.md @@ -0,0 +1,131 @@ +--- +title: Directives +id: version-2.0.0-beta.3-directives +original_id: directives +--- + +> A directive is an identifier preceded by a `@` character, optionally followed by a list of named arguments, which can appear after almost any form of syntax in the GraphQL query or schema languages. + +Though the [GraphQL directives](https://www.apollographql.com/docs/graphql-tools/schema-directives) syntax is similar to TS decorators, they are purely an SDL (Schema Definition Language) feature that allows you to add metadata to a selected type or its field: + +```graphql +type Foo @auth(requires: USER) { + field: String! +} + +type Bar { + field: String! @auth(requires: USER) +} +``` + +That metadata can be read at runtime to modify the structure and behavior of a GraphQL schema to support reusable code and tasks like authentication, permission, formatting, and plenty more. They are also really useful for some external services like [Apollo Cache Control](https://www.apollographql.com/docs/apollo-server/performance/caching/#adding-cache-hints-statically-in-your-schema) or [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/#federated-schema-example). + +**TypeGraphQL** of course provides some basic support for using the schema directives via the `@Directive` decorator. + +## Usage + +### Declaring in schema + +Basically, we declare the usage of directives just like in SDL, with the `@` syntax: + +```ts +@Directive('@deprecated(reason: "Use newField")') +``` + +Currently, you can use the directives only on object types, input types, interface types and their fields or fields resolvers, as well as queries, mutations and subscriptions. Other locations like scalars, enums, unions or arguments are not yet supported. + +So the `@Directive` decorator can be placed over the class property/method or over the type class itself, depending on the needs and the placements supported by the implementation: + +```ts +@Directive("@auth(requires: USER)") +@ObjectType() +class Foo { + @Field() + field: string; +} + +@ObjectType() +class Bar { + @Directive("@auth(requires: USER)") + @Field() + field: string; +} + +@Resolver(of => Foo) +class FooBarResolver { + @Directive("@auth(requires: ANY)") + @Query() + foobar(@Arg("baz") baz: string): string { + return "foobar"; + } + + @Directive("@auth(requires: ADMIN)") + @FieldResolver() + bar(): string { + return "foobar"; + } +} +``` + +> Note that even as directives are a purely SDL thing, they won't appear in the generated schema definition file. Current implementation of directives in TypeGraphQL is using some crazy workarounds because [`graphql-js` doesn't support setting them by code](https://github.com/graphql/graphql-js/issues/1343) and the built-in `printSchema` utility omits the directives while printing. See [emit schema with custom directives](./emit-schema.md#emit-schema-with-custom-directives) for more info. + +Also please note that `@Directive` can only contain a single GraphQL directive name or declaration. If you need to have multiple directives declared, just place multiple decorators: + +```ts +@ObjectType() +class Foo { + @Directive("@lowercase") + @Directive('@deprecated(reason: "Use `newField`")') + @Directive("@hasRole(role: Manager)") + @Field() + bar: string; +} +``` + +### Providing the implementation + +Besides declaring the usage of directives, you also have to register the runtime part of the used directives. + +> Be aware that TypeGraphQL doesn't have any special way for implementing schema directives. You should use some [3rd party libraries](https://the-guild.dev/graphql/tools/docs/schema-directives#implementing-schema-directives) depending on the tool set you use in your project, e.g. `@graphql-tools/*` or `ApolloServer`. + +If you write your custom GraphQL directive or import a package that exports a `GraphQLDirective` instance, you need to register the directives definitions in the `buildSchema` options: + +```ts +// Build TypeGraphQL executable schema +const tempSchema = await buildSchema({ + resolvers: [SampleResolver], + // Register the directives definitions + directives: [myDirective], +}); +``` + +Then you need to apply the schema transformer for your directive, that implements the desired logic of your directive: + +```ts +// Transform and obtain the final schema +const schema = myDirectiveTransformer(tempSchema); +``` + +If the directive package used by you exports a string-based `typeDefs`, you need to add those typedefs to the schema and then apply directive transformer. + +Here is an example using the [`@graphql-tools/*`](https://the-guild.dev/graphql/tools): + +```ts +import { mergeSchemas } from "@graphql-tools/schema"; +import { renameDirective } from "fake-rename-directive-package"; + +// Build TypeGraphQL executable schema +const schemaSimple = await buildSchema({ + resolvers: [SampleResolver], +}); + +// Merge schema with sample directive type definitions +const schemaMerged = mergeSchemas({ + schemas: [schemaSimple], + // Register the directives definitions + typeDefs: [renameDirective.typeDefs], +}); + +// Transform and obtain the final schema +const schema = renameDirective.transformer(schemaMerged); +``` diff --git a/website/versioned_docs/version-2.0.0-beta.3/emit-schema.md b/website/versioned_docs/version-2.0.0-beta.3/emit-schema.md new file mode 100644 index 000000000..7557922b7 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/emit-schema.md @@ -0,0 +1,66 @@ +--- +title: Emitting the schema SDL +id: version-2.0.0-beta.3-emit-schema +original_id: emit-schema +--- + +TypeGraphQL's main feature is creating the schema using only TypeScript classes and decorators. However, there might be a need for the schema to be printed into a `schema.graphql` file and there are plenty of reasons for that. Mainly, the schema SDL file is needed for GraphQL ecosystem tools that perform client-side queries autocompletion and validation. Some developers also may want to use it as a kind of snapshot for detecting schema regression or they just prefer to read the SDL file to explore the API instead of reading the complicated TypeGraphQL-based app code, navigating through the GraphiQL or GraphQL Playground. To accomplish this demand, TypeGraphQL allows you to create a schema definition file in two ways. + +The first one is to generate it automatically on every build of the schema - just pass `emitSchemaFile: true` to the `buildSchema` options in order to emit the `schema.graphql` in the root of the project's working directory. You can also manually specify the path and the file name where the schema definition should be written or even specify `PrintSchemaOptions` to configure the look and format of the schema definition. + +```ts +const schema = await buildSchema({ + resolvers: [ExampleResolver], + // Automatically create `schema.graphql` file with schema definition in project's working directory + emitSchemaFile: true, + // Or create the file with schema in selected path + emitSchemaFile: path.resolve(__dirname, "__snapshots__/schema/schema.graphql"), + // Or pass a config object + emitSchemaFile: { + path: __dirname + "/schema.graphql", + sortedSchema: false, // By default the printed schema is sorted alphabetically + }, +}); +``` + +The second way to emit the schema definition file is by doing it programmatically. We would use the `emitSchemaDefinitionFile` function (or it's sync version `emitSchemaDefinitionFileSync`) and pass in the path, along with the schema object. We can use this among others as part of a testing script that checks if the snapshot of the schema definition is correct or to automatically generate it on every file change during local development. + +```ts +import { emitSchemaDefinitionFile } from "type-graphql"; + +// ... +hypotheticalFileWatcher.watch("./src/**/*.{resolver,type,input,arg}.ts", async () => { + const schema = getSchemaNotFromBuildSchemaFunction(); + await emitSchemaDefinitionFile("/path/to/folder/schema.graphql", schema); +}); +``` + +### Emit schema with custom directives + +Currently TypeGraphQL does not directly support emitting the schema with custom directives due to `printSchema` function limitations from `graphql-js`. + +If we want the custom directives to appear in the generated schema definition file we have to create a custom function that use a third-party `printSchema` function. + +Below there is an example that uses the `printSchemaWithDirectives` function from [`@graphql-tools/utils`](https://www.graphql-tools.com/docs/api/modules/utils): + +```ts +import { GraphQLSchema, lexicographicSortSchema } from "graphql"; +import { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { outputFile } from "type-graphql/dist/helpers/filesystem"; + +export async function emitSchemaDefinitionWithDirectivesFile( + schemaFilePath: string, + schema: GraphQLSchema, +): Promise { + const schemaFileContent = printSchemaWithDirectives(lexicographicSortSchema(schema)); + await outputFile(schemaFilePath, schemaFileContent); +} +``` + +The usage of `emitSchemaDefinitionWithDirectivesFile` function is the same as with standard `emitSchemaDefinitionFile`: + +```ts +const schema = await buildSchema(/*...*/); + +await emitSchemaDefinitionWithDirectivesFile("/path/to/folder/schema.graphql", schema); +``` diff --git a/website/versioned_docs/version-2.0.0-beta.3/enums.md b/website/versioned_docs/version-2.0.0-beta.3/enums.md new file mode 100644 index 000000000..498e9ff0a --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/enums.md @@ -0,0 +1,143 @@ +--- +title: Enums +id: version-2.0.0-beta.3-enums +original_id: enums +--- + +Nowadays almost all typed languages have support for enumerated types, including TypeScript. Enums limit the range of a variable's values to a set of predefined constants, which makes it easier to document intent. + +GraphQL also has enum type support, so TypeGraphQL allows us to use TypeScript enums in our GraphQL schema. + +## Creating enum + +Let's create a TypeScript enum. It can be a numeric or string enum - the internal values of enums are taken from the enum definition values and the public names taken from the enum keys: + +```ts +// Implicit value 0, 1, 2, 3 +enum Direction { + UP, + DOWN, + LEFT, + RIGHT, +} + +// Or explicit values +enum Direction { + UP = "UP", + DOWN = "DOWN", + LEFT = "LEFT", + RIGHT = "RIGHT", +} +``` + +To tell TypeGraphQL about our enum, we would ideally mark the enums with the `@EnumType()` decorator. However, TypeScript decorators only work with classes, so we need to make TypeGraphQL aware of the enums manually by calling the `registerEnumType` function and providing the enum name for GraphQL: + +```ts +import { registerEnumType } from "type-graphql"; + +registerEnumType(Direction, { + name: "Direction", // Mandatory + description: "The basic directions", // Optional +}); +``` + +In case we need to provide additional GraphQL-related config for values, like description or deprecation reason, we can use `valuesConfig` property and put the data inside it, e.g.: + +```ts +enum Direction { + UP = "UP", + DOWN = "DOWN", + LEFT = "LEFT", + RIGHT = "RIGHT", + SIDEWAYS = "SIDEWAYS", +} + +registerEnumType(Direction, { + name: "Direction", + description: "The basic directions", + valuesConfig: { + SIDEWAYS: { + deprecationReason: "Replaced with Left or Right", + }, + RIGHT: { + description: "The other left", + }, + }, +}); +``` + +This way, the additional info will be emitted in the GraphQL schema: + +```graphql +enum Direction { + UP + DOWN + LEFT + """ + The other left + """ + RIGHT + SIDEWAYS @deprecated(reason: "Replaced with Left or Right") +} +``` + +## Using enum + +The last step is very important: TypeScript has limited reflection ability, so this is a case where we have to explicitly provide the enum type for object type fields, input type fields, args, and the return type of queries and mutations: + +```ts +@InputType() +class JourneyInput { + @Field(type => Direction) // Mandatory + direction: Direction; +} +``` + +Without this annotation, the generated GQL type would be `String` or `Float` (depending on the enum type), rather than the `ENUM` we are aiming for. + +With all that in place, we can use our enum directly in our code ๐Ÿ˜‰ + +```ts +@Resolver() +class SpriteResolver { + private sprite = getMarioSprite(); + + @Mutation() + move(@Arg("direction", type => Direction) direction: Direction): boolean { + switch (direction) { + case Direction.Up: + this.sprite.position.y++; + break; + case Direction.Down: + this.sprite.position.y--; + break; + case Direction.Left: + this.sprite.position.x--; + break; + case Direction.Right: + this.sprite.position.x++; + break; + default: + // Never reached + return false; + } + + return true; + } +} +``` + +## Interoperability + +Enums in TypeGraphQL are designed with server side in mind - the runtime will map the string value from input into a corresponding enum value, like `"UP"` into `0`. While this is very handy e.g. for mapping database values into GraphQL API enum names, it makes it unusable on the query side because `Direction.UP` will put `0` in the query which is an invalid value (should be `UP`). + +So if we would like to share the types definition and use the enum on the client side app or use the enums directly on the server app e.g. in tests, we have to use the direct mapping of the enum member names with values, e.g.: + +```ts +enum Direction { + UP = "UP", + DOWN = "DOWN", + LEFT = "LEFT", + RIGHT = "RIGHT", +} +``` diff --git a/website/versioned_docs/version-2.0.0-beta.3/esm.md b/website/versioned_docs/version-2.0.0-beta.3/esm.md new file mode 100644 index 000000000..70d110d0a --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/esm.md @@ -0,0 +1,48 @@ +--- +title: ECMAScript Modules +id: version-2.0.0-beta.3-esm +original_id: esm +--- + +Since `v2.0.0` release, TypeGraphQL is compatible with ECMAScript modules. + +Thanks to this, we can `import` the `type-graphql` package in the ESM projects without any hassle. + +## TypeScript configuration + +It's important to properly configure the project, so that it uses ESM correctly: + +- the `module` options should be set to `ES2020/ES2022/ESNext` +- for the NodeJS apps, we should set `moduleResolution` to `"node16"` + +All in all, the `tsconfig.json` file should looks like this: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "es2021", + "module": "es2020", + "moduleResolution": "node16", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} +``` + +## Package.json configuration + +It is also important to set `type` option to `"module"` in your `package.json` file: + +```json title="package.json" +{ + "type": "module" +} +``` + +## Imports + +Apart from using `import` syntax, your local imports have to use the `.js` suffix, e.g.: + +```ts +import { MyResolver } from "./resolvers/MyResolver.js"; +``` diff --git a/website/versioned_docs/version-2.0.0-beta.3/examples.md b/website/versioned_docs/version-2.0.0-beta.3/examples.md new file mode 100644 index 000000000..ce7987bf4 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/examples.md @@ -0,0 +1,52 @@ +--- +title: Examples +sidebar_label: List of examples +id: version-2.0.0-beta.3-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-beta.3/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-beta.3/examples/simple-usage) + +## Advanced + +- [Enums and unions](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/enums-and-unions) +- [Subscriptions (simple)](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/simple-subscriptions) +- [Subscriptions (using Redis) \*\*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/redis-subscriptions) +- [Interfaces](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/interfaces-inheritance) +- [Extensions (metadata)](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/extensions) + +## Features usage + +- [Dependency injection (IoC container)](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/using-container) + - [Scoped containers](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/using-scoped-container) +- [Authorization](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/authorization) +- [Validation](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/automatic-validation) + - [Custom validation](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/custom-validation) +- [Types inheritance](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/interfaces-inheritance) +- [Resolvers inheritance](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/resolvers-inheritance) +- [Generic types](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/generic-types) +- [Mixin classes](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/mixin-classes) +- [Middlewares and Custom Decorators](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/middlewares-custom-decorators) +- [Query complexity](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/query-complexity) + +## 3rd party libs integration + +- [TypeORM (manual, synchronous) \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/typeorm-basic-usage) +- [TypeORM (automatic, lazy relations) \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/typeorm-lazy-relations) +- [MikroORM \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/mikro-orm) +- [Typegoose \*](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/typegoose) +- [Apollo federation](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/apollo-federation) +- [Apollo Cache Control](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/apollo-cache) +- [GraphQL Scalars](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/examples/graphql-scalars) +- [TSyringe](https://github.com/MichalLytek/type-graphql/tree/v2.0.0-beta.3/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-beta.3/extensions.md b/website/versioned_docs/version-2.0.0-beta.3/extensions.md new file mode 100644 index 000000000..90f7b0a29 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/extensions.md @@ -0,0 +1,119 @@ +--- +title: Extensions +id: version-2.0.0-beta.3-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-beta.3/examples/extensions). diff --git a/website/versioned_docs/version-2.0.0-beta.3/faq.md b/website/versioned_docs/version-2.0.0-beta.3/faq.md new file mode 100644 index 000000000..ab1ab30ef --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/faq.md @@ -0,0 +1,93 @@ +--- +title: Frequently Asked Questions +id: version-2.0.0-beta.3-faq +original_id: faq +--- + +## Resolvers + +### Should I implement a field resolver as an object type getter, a method or a resolver class method? + +This depends on various factors: + +- if the resolver only needs access to the root/object value - use a getter +- if the field has arguments + - and must perform side effects e.g. a database call - use a resolver class method and leverage the dependency injection mechanism + - otherwise, use object type methods (pure functions, calculations based on object values and arguments) +- if the business logic must be separated from the type definition - use a resolver class method + +### Are there any global error handlers to catch errors from resolvers or services? + +Use middleware for this purpose - just wrap `await next()` in a try-catch block then register it as the first global middleware. + +### Why did I receive this error? `GraphQLError: Expected value of type "MyType" but got: [object Object]` + +This error occurs when the resolver (query, mutation, field) type is an interface/union and a plain object is returned from it. +In this case, what should be returned is an instance of the selected object type class in the resolver. +Otherwise, `graphql-js` will not be able to correctly detect the underlying GraphQL type. + +## Bootstrapping + +### How do I fix this error? `Cannot use GraphQLSchema "[object Object]" from another module or realm` + +This error occurs mostly when there are more than one version of the `graphql-js` module in the project. +In most cases it means that one of our dependencies has a dependency on a different version of `graphql-js`, e.g. we, or TypeGraphQL use `v14.0.2` but `apollo-server-express` depends on `v0.13.2`. +We can print the dependency tree by running `npm ls graphql` (or the yarn equivalent) to find the faulty dependencies. +Then we should update or downgrade them until they all match the semver on `graphql`, e.g. `^14.0.0`. +Dependencies may also need to be flattened, so that they all share a single instance of the `graphql` module in the `node_modules` directory - to achieve this, just run `npm dedupe` (or the yarn equivalent). + +The same rule applies to this error: `node_modules/type-graphql/node_modules/@types/graphql/type/schema").GraphQLSchema' is not assignable to type 'import("node_modules/@types/graphql/type/schema").GraphQLSchema'`. +In this case we repeat the same checks but for the `@types/graphql` module in our dependencies. + +## Types + +### Is `@InputType()` different from `@ArgsType()`? + +Of course! +`@InputType` will generate a real `GraphQLInputType` type and should be used when we need a nested object in the args: + +```graphql +updateItem(data: UpdateItemInput!): Item! +``` + +`@ArgsType` is virtual and it will be flattened in schema: + +```graphql +updateItem(id: Int!, userId: Int!): Item! +``` + +### When should I use the `() => [ItemType]` syntax? + +We should use the `[ItemType]` syntax any time the field type or the return type is an array from a query or mutation. + +Even though technically the array notation can be omitted (when the base type is not `Promise`) and only provide the type of array item (e.g. `@Field(() => ItemType) field: ItemType[]`) - it's better to be consistent with other annotations by explicitly defining the type. + +### How can I define a tuple? + +Unfortunately, [GraphQL spec doesn't support tuples](https://github.com/graphql/graphql-spec/issues/423), so you can't just use `data: [Int, Float]` as a GraphQL type. + +Instead, you have to create a transient object (or input) type that fits your data, e.g.: + +```graphql +type DataPoint { + x: Int + y: Float +} +``` + +and then use it in the list type as your GraphQL type: + +```graphql +data: [DataPoint] +``` + +### Situations frequently arise where InputType and ObjectType have exactly the same shape. How can I share the definitions? + +In GraphQL, input objects have a separate type in the system because object types can contain fields that express circular references or references to interfaces and unions, neither of which are appropriate for use as input arguments. +However, if there are only simple fields in the class definition, reuse the code between the InputType and the ObjectType by decorating the ObjectType class with `@InputType`. Remember to set a new name of the type in the decorator parameter: + +```ts +@ObjectType() // Name inferred as 'Person' from class name +@InputType("PersonInput") +export class Person {} +``` diff --git a/website/versioned_docs/version-2.0.0-beta.3/generic-types.md b/website/versioned_docs/version-2.0.0-beta.3/generic-types.md new file mode 100644 index 000000000..1d501be8b --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/generic-types.md @@ -0,0 +1,169 @@ +--- +title: Generic Types +id: version-2.0.0-beta.3-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-beta.3/examples/generic-types). diff --git a/website/versioned_docs/version-2.0.0-beta.3/getting-started.md b/website/versioned_docs/version-2.0.0-beta.3/getting-started.md new file mode 100644 index 000000000..b2e8ab4ac --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/getting-started.md @@ -0,0 +1,190 @@ +--- +title: Getting started +id: version-2.0.0-beta.3-getting-started +original_id: getting-started +--- + +> Make sure you've completed all the steps described in the [installation instructions](./installation.md). + +To explore all of the powerful capabilities of TypeGraphQL, we will create a sample GraphQL API for cooking recipes. + +Let's start with the `Recipe` type, which is the foundation of our API. + +## Types + +Our goal is to get the equivalent of this type described in SDL: + +```graphql +type Recipe { + id: ID! + title: String! + description: String + creationDate: Date! + ingredients: [String!]! +} +``` + +So we create the `Recipe` class with all its properties and types: + +```ts +class Recipe { + id: string; + title: string; + description?: string; + creationDate: Date; + ingredients: string[]; +} +``` + +Then we decorate the class and its properties with decorators: + +```ts +@ObjectType() +class Recipe { + @Field(type => ID) + id: string; + + @Field() + title: string; + + @Field({ nullable: true }) + description?: string; + + @Field() + creationDate: Date; + + @Field(type => [String]) + ingredients: string[]; +} +``` + +The detailed rules of when to use `nullable`, `array` and others are described in the [fields and types docs](./types-and-fields.md). + +## Resolvers + +After that we want to create typical crud queries and mutations. To do so, we create the resolver (controller) class that will have injected the `RecipeService` in the constructor: + +```ts +@Resolver(Recipe) +class RecipeResolver { + constructor(private recipeService: RecipeService) {} + + @Query(returns => Recipe) + async recipe(@Arg("id") id: string) { + const recipe = await this.recipeService.findById(id); + if (recipe === undefined) { + throw new RecipeNotFoundError(id); + } + return recipe; + } + + @Query(returns => [Recipe]) + recipes(@Args() { skip, take }: RecipesArgs) { + return this.recipeService.findAll({ skip, take }); + } + + @Mutation(returns => Recipe) + @Authorized() + addRecipe( + @Arg("newRecipeData") newRecipeData: NewRecipeInput, + @Ctx("user") user: User, + ): Promise { + return this.recipeService.addNew({ data: newRecipeData, user }); + } + + @Mutation(returns => Boolean) + @Authorized(Roles.Admin) + async removeRecipe(@Arg("id") id: string) { + try { + await this.recipeService.removeById(id); + return true; + } catch { + return false; + } + } +} +``` + +We use the `@Authorized()` decorator to restrict access to authorized users only or the users that fulfil the roles requirements. +The detailed rules for when and why we declare `returns => Recipe` functions and others are described in [resolvers docs](./resolvers.md). + +## Inputs and Arguments + +Ok, but what are `NewRecipeInput` and `RecipesArgs`? They are, of course, classes: + +```ts +@InputType() +class NewRecipeInput { + @Field() + @MaxLength(30) + title: string; + + @Field({ nullable: true }) + @Length(30, 255) + description?: string; + + @Field(type => [String]) + @ArrayMaxSize(30) + ingredients: string[]; +} + +@ArgsType() +class RecipesArgs { + @Field(type => Int) + @Min(0) + skip: number = 0; + + @Field(type => Int) + @Min(1) + @Max(50) + take: number = 25; +} +``` + +`@Length`, `@Min` and `@ArrayMaxSize` are decorators from [`class-validator`](https://github.com/typestack/class-validator) that automatically perform field validation in TypeGraphQL. + +## Building schema + +The last step that needs to be done is to actually build the schema from the TypeGraphQL definition. We use the `buildSchema` function for this: + +```ts +const schema = await buildSchema({ + resolvers: [RecipeResolver], +}); + +// ... Server +``` + +Et voilร ! Now we have fully functional GraphQL schema! +If we print it, this is how it would look: + +```graphql +type Recipe { + id: ID! + title: String! + description: String + creationDate: Date! + ingredients: [String!]! +} +input NewRecipeInput { + title: String! + description: String + ingredients: [String!]! +} +type Query { + recipe(id: ID!): Recipe + recipes(skip: Int = 0, take: Int = 25): [Recipe!]! +} +type Mutation { + addRecipe(newRecipeData: NewRecipeInput!): Recipe! + removeRecipe(id: ID!): Boolean! +} +``` + +## Want more? + +That was only the tip of the iceberg - a very simple example with basic GraphQL types. Do you use interfaces, enums, unions and custom scalars? That's great because TypeGraphQL fully supports them too! There are also more advanced concepts like the authorization checker, inheritance support and field resolvers. + +A lot of these topics are covered in [Ben Awad](https://github.com/benawad)'s [TypeGraphQL video series](https://www.youtube.com/playlist?list=PLN3n1USn4xlma1bBu3Tloe4NyYn9Ko8Gs) on YouTube. + +For more complicated cases, go to the [Examples section](./examples.md) where you can discover e.g. how well TypeGraphQL integrates with TypeORM. diff --git a/website/versioned_docs/version-2.0.0-beta.3/inheritance.md b/website/versioned_docs/version-2.0.0-beta.3/inheritance.md new file mode 100644 index 000000000..48dbe6721 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/inheritance.md @@ -0,0 +1,145 @@ +--- +title: Inheritance +id: version-2.0.0-beta.3-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-beta.3/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-beta.3/examples/resolvers-inheritance). diff --git a/website/versioned_docs/version-2.0.0-beta.3/installation.md b/website/versioned_docs/version-2.0.0-beta.3/installation.md new file mode 100644 index 000000000..3d2ef08a9 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/installation.md @@ -0,0 +1,63 @@ +--- +title: Installation +id: version-2.0.0-beta.3-installation +original_id: installation +--- + +Before getting started with TypeGraphQL we need to install some additional dependencies and properly configure the TypeScript configuration for our project. + +> #### Prerequisites +> +> Before we begin, we must make sure our development environment includes Node.js and npm. + +## Packages installation + +First, we have to install the main package, as well as [`graphql-js`](https://github.com/graphql/graphql-js) and [`graphql-scalars`](https://github.com/urigo/graphql-scalars) which are peer dependencies of TypeGraphQL: + +```sh +npm install graphql graphql-scalars type-graphql +``` + +Also, the `reflect-metadata` shim is required to make the type reflection work: + +```sh +npm install reflect-metadata +``` + +We must ensure that it is imported at the top of our entry file (before we use/import `type-graphql` or our resolvers): + +```ts +import "reflect-metadata"; +``` + +## TypeScript configuration + +It's important to set these options in the `tsconfig.json` file of our project: + +```json +{ + "emitDecoratorMetadata": true, + "experimentalDecorators": true +} +``` + +`TypeGraphQL` is designed to work with Node.js LTS and the latest stable releases. It uses features from ES2021 so we should set our `tsconfig.json` file appropriately: + +```js +{ + "target": "es2021" // Or newer if Node.js version supports it +} +``` + +All in all, the minimal `tsconfig.json` file example looks like this: + +```json +{ + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} +``` diff --git a/website/versioned_docs/version-2.0.0-beta.3/interfaces.md b/website/versioned_docs/version-2.0.0-beta.3/interfaces.md new file mode 100644 index 000000000..c7e5b6b63 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/interfaces.md @@ -0,0 +1,258 @@ +--- +title: Interfaces +id: version-2.0.0-beta.3-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-beta.3/examples/interfaces-inheritance). diff --git a/website/versioned_docs/version-2.0.0-beta.3/introduction.md b/website/versioned_docs/version-2.0.0-beta.3/introduction.md new file mode 100644 index 000000000..26cd5ed09 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/introduction.md @@ -0,0 +1,59 @@ +--- +title: Introduction +sidebar_label: What & Why +id: version-2.0.0-beta.3-introduction +original_id: introduction +--- + +We all love GraphQL! It's really great and solves many problems that we have with REST APIs, such as overfetching and underfetching. But developing a GraphQL API in Node.js with TypeScript is sometimes a bit of a pain. + +## What? + +**TypeGraphQL** is a library that makes this process enjoyable by defining the schema using only classes and a bit of decorator magic. +Example object type: + +```ts +@ObjectType() +class Recipe { + @Field() + title: string; + + @Field(type => [Rate]) + ratings: Rate[]; + + @Field({ nullable: true }) + averageRating?: number; +} +``` + +It also has a set of useful features, like validation, authorization and dependency injection, which helps develop GraphQL APIs quickly & easily! + +## Why? + +As mentioned, developing a GraphQL API in Node.js with TypeScript is sometimes a bit of a pain. +Why? Let's take a look at the steps we usually have to take. + +First, we create all the schema types in SDL. We also create our data models using [ORM classes](https://github.com/typeorm/typeorm), which represent our database entities. Then we start to write resolvers for our queries, mutations and fields. This forces us, however, to begin with creating TypeScript interfaces for all arguments and inputs and/or object types. After that, we can actually implement the resolvers, using weird generic signatures, e.g.: + +```ts +export const getRecipesResolver: GraphQLFieldResolver = async ( + _, + args, + ctx, +) => { + // Common tasks repeatable for almost every resolver + const auth = Container.get(AuthService); + if (!auth.check(ctx.user)) { + throw new NotAuthorizedError(); + } + await joi.validate(getRecipesSchema, args); + const repository = TypeORM.getRepository(Recipe); + + // Business logic, e.g.: + return repository.find({ skip: args.offset, take: args.limit }); +}; +``` + +The biggest problem is code redundancy which makes it difficult to keep things in sync. To add a new field to our entity, we have to jump through all the files: modify the entity class, then modify the schema, and finally update the interface. The same goes with inputs or arguments: it's easy to forget to update one of them or make a mistake with a type. Also, what if we've made a typo in a field name? The rename feature (F2) won't work correctly. + +**TypeGraphQL** comes to address these issues, based on experience from a few years of developing GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using classes and a bit of decorator help. Additional features like dependency injection, validation and auth guards help with common tasks that would normally have to be handled by ourselves. diff --git a/website/versioned_docs/version-2.0.0-beta.3/middlewares.md b/website/versioned_docs/version-2.0.0-beta.3/middlewares.md new file mode 100644 index 000000000..5dc20c1b2 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/middlewares.md @@ -0,0 +1,189 @@ +--- +title: Middleware and guards +id: version-2.0.0-beta.3-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, place the `@UseMiddleware()` decorator above the field or resolver 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(); + } +} +``` + +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 middleware like measuring resolve time or catching errors, it might be annoying to place a `@UseMiddleware(ResolveTime)` decorator on every field/resolver. + +Hence, in TypeGraphQL we can also register a global middleware that will be called for each query, mutation, subscription and field resolver. 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-beta.3/examples/middlewares-custom-decorators). diff --git a/website/versioned_docs/version-2.0.0-beta.3/nestjs.md b/website/versioned_docs/version-2.0.0-beta.3/nestjs.md new file mode 100644 index 000000000..eff10ba65 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/nestjs.md @@ -0,0 +1,49 @@ +--- +title: NestJS Integration +sidebar_label: NestJS +id: version-2.0.0-beta.3-nestjs +original_id: nestjs +--- + +TypeGraphQL provides some basic integration with NestJS by the [`typegraphql-nestjs` package](https://www.npmjs.com/package/typegraphql-nestjs). + +It allows to use TypeGraphQL features while integrating with NestJS modules system and its dependency injector. + +## Overview + +The usage is similar to the official `@nestjs/graphql` package. +First you need to register your resolver classes in `providers` of the `@Module` : + +```ts +@Module({ + providers: [RecipeResolver, RecipeService], +}) +export default class RecipeModule {} +``` + +Then you need to register the TypeGraphQL module in your root module - you can pass there all standard `buildSchema` options: + +```ts +@Module({ + imports: [ + TypeGraphQLModule.forRoot({ + emitSchemaFile: true, + authChecker, + context: ({ req }) => ({ currentUser: req.user }), + }), + RecipeModule, + ], +}) +export default class AppModule {} +``` + +And your `AppModule` is ready to use like with a standard NestJS approach. + +### Caveats + +For now, this basic integration doesn't support other NestJS features like guards, interceptors, filters and pipes. +To achieve the same goals, you can use standard TypeGraphQL equivalents - middlewares, custom decorators, built-in authorization and validation. + +## Documentation and examples + +You can find some examples and more detailed info about the installation and the usage [in the separate GitHub repository](https://github.com/MichalLytek/typegraphql-nestjs). diff --git a/website/versioned_docs/version-2.0.0-beta.3/performance.md b/website/versioned_docs/version-2.0.0-beta.3/performance.md new file mode 100644 index 000000000..8bfa9e8da --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/performance.md @@ -0,0 +1,83 @@ +--- +title: Performance +id: version-2.0.0-beta.3-performance +original_id: performance +--- + +**TypeGraphQL** is basically an abstraction layer built on top of the reference GraphQL implementation for Javascript - [`graphql-js`](https://github.com/graphql/graphql-js). It not only allows for building a GraphQL schema using classes and decorators but also gives a set of tools that focus on the developer experience and allows for making common tasks easily - authorization, validation, custom middlewares and others. + +While this enable easy and convenient development, it's sometimes a tradeoff in a performance. + +## Benchmarks + +To measure the overhead of the abstraction, a few demo examples were made to compare the usage of TypeGraphQL against the implementations using "bare metal" - raw `graphql-js` library. The benchmarks are located in a [folder on the GitHub repo](../benchmarks). + +The most demanding cases like returning an array of 25 000 nested objects showed that in some cases it might be about 5 times slower. + +| | 25 000 array items | Deeply nested object | +| -------------------- | :----------------: | :------------------: | +| Standard TypeGraphQL | 1253.28 ms | 45.57 ฮผs | +| `graphql-js` | 265.52 ms | 24.22 ฮผs | + +In real apps (e.g. with complex database queries) it's usually a much lower factor but still not negligible. That's why TypeGraphQL has some built-in performance optimization options. + +## Optimizations + +Promises in JS have a quite big performance overhead. In the same example of returning an array with 25 000 items, if we change the Object Type field resolvers to an asynchronous one that return a promise, the execution slows down by a half even in "raw" `graphql-js`. + +| `graphql-js` | 25 000 array items | +| --------------- | :----------------: | +| sync resolvers | 265.52 ms | +| async resolvers | 512.61 ms | + +TypeGraphQL tries to avoid the async execution path when it's possible, e.g. if the query/mutation/field resolver doesn't use the auth feature, doesn't use args (or has args validation disabled) and if doesn't return a promise. So if you find a bottleneck in your app, try to investigate your resolvers, disable not used features and maybe remove some unnecessary async/await usage. + +Also, using middlewares implicitly turns on the async execution path (for global middlewares the middlewares stack is created even for every implicit field resolver!), so be careful when using this feature if you care about the performance very much (and maybe then use the "simple resolvers" tweak described below). + +The whole middleware stack will be soon redesigned with a performance in mind and with a new API that will also allow fine-grained scoping of global middlewares. Stay tuned! + +## Further performance tweaks + +When we have a query that returns a huge amount of JSON-like data and we don't need any field-level access control or other custom middlewares, we can turn off the whole authorization and middlewares stack for selected field resolver using a `{ simple: true }` decorator option, e.g.: + +```ts +@ObjectType() +class SampleObject { + @Field() + sampleField: string; + + @Field({ simple: true }) + publicFrequentlyQueriedField: SomeType; +} +``` + +Moreover, we can also apply this behavior for all the fields of the object type by using a `{ simpleResolvers: true }` decorator option, e.g.: + +```ts +@ObjectType({ simpleResolvers: true }) +class Post { + @Field() + title: string; + + @Field() + createdAt: Date; + + @Field() + isPublished: boolean; +} +``` + +This simple trick can speed up the execution up to 76%! The benchmarks show that using simple resolvers allows for as fast execution as with bare `graphql-js` - the measured overhead is only about ~13%, which is a much more reasonable value than 500%. Below you can see [the benchmarks results](../benchmarks): + +| | 25 000 array items | +| ------------------------------------------------------------------------ | :----------------: | +| `graphql-js` | 265.52 ms | +| Standard TypeGraphQL | 310.36 ms | +| TypeGraphQL with a global middleware | 1253.28 ms | +| **TypeGraphQL with "simpleResolvers" applied (and a global middleware)** | **299.61 ms** | + +> This optimization **is not turned on by default** mostly because of the global middlewares and authorization feature. + +By using "simple resolvers" we are turning them off, so we have to be aware of the consequences - `@Authorized` guard on fields won't work for that fields so they will be publicly available, as well as global middlewares won't be executed for that fields, so we might lost, for example, performance metrics or access logs. + +That's why we should **be really careful with using this tweak**. The rule of thumb is to use "simple resolvers" only when it's really needed, like returning huge array of nested objects. diff --git a/website/versioned_docs/version-2.0.0-beta.3/prisma.md b/website/versioned_docs/version-2.0.0-beta.3/prisma.md new file mode 100644 index 000000000..cbe1f0ea3 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/prisma.md @@ -0,0 +1,54 @@ +--- +title: Prisma Integration +sidebar_label: Prisma +id: version-2.0.0-beta.3-prisma +original_id: prisma +--- + +TypeGraphQL provides an integration with Prisma by the [`typegraphql-prisma` package](https://www.npmjs.com/package/typegraphql-prisma). + +It generates the type classes and CRUD resolvers based on the Prisma schema, so we can execute complex queries or mutations that corresponds to the Prisma actions, without having to write any code for that. + +## Overview + +To make use of the prisma integration, first we need to add a new generator to the `schema.prisma` file: + +```sh +generator typegraphql { + provider = "typegraphql-prisma" +} +``` + +Then, after running `prisma generate` we can import the generated resolvers classes and use them to build our schema: + +```ts +import { resolvers } from "@generated/type-graphql"; + +const schema = await buildSchema({ + resolvers, + validate: false, +}); +``` + +So we will be able to execute a complex query, that talks with the real database, in just a few minutes! + +```graphql +query GetSomeUsers { + users(where: { email: { contains: "prisma" } }, orderBy: { name: desc }) { + id + name + email + posts(take: 10, orderBy: { updatedAt: desc }) { + published + title + content + } + } +} +``` + +## Documentation and examples + +To read about all the `typegraphql-prisma` features, like exposing selected Prisma actions or changing exposed model type name, as well as how to write a custom query or how to add some fields to model type, please check the docs [on the dedicated website](https://prisma.typegraphql.com). + +There also can be found the links to some examples and more detailed info about the installation and the configuration. diff --git a/website/versioned_docs/version-2.0.0-beta.3/resolvers.md b/website/versioned_docs/version-2.0.0-beta.3/resolvers.md new file mode 100644 index 000000000..dab338abc --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/resolvers.md @@ -0,0 +1,352 @@ +--- +title: Resolvers +id: version-2.0.0-beta.3-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-beta.3/examples). diff --git a/website/versioned_docs/version-2.0.0-beta.3/scalars.md b/website/versioned_docs/version-2.0.0-beta.3/scalars.md new file mode 100644 index 000000000..ced4d34c9 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/scalars.md @@ -0,0 +1,175 @@ +--- +title: Scalars +id: version-2.0.0-beta.3-scalars +original_id: scalars +--- + +## Aliases + +TypeGraphQL provides aliases for 3 basic scalars: + +- Int --> GraphQLInt; +- Float --> GraphQLFloat; +- ID --> GraphQLID; + +This shorthand allows you to save keystrokes when declaring field types: + +```ts +// Import the aliases +import { ID, Float, Int } from "type-graphql"; + +@ObjectType() +class MysteryObject { + @Field(type => ID) + readonly id: string; + + @Field(type => Int) + notificationsCount: number; + + @Field(type => Float) + probability: number; +} +``` + +In the last case you can omit the `type => Float` since JavaScript `Number` will become `GraphQLFloat` in the schema automatically. + +Other scalars - i.e. `GraphQLString` and `GraphQLBoolean` - do not need aliases. When possible, they will be reflected automatically: + +```ts +@ObjectType() +class User { + @Field() + name: string; + + @Field() + isOld: boolean; +} +``` + +However in some cases we must explicitly declare the string/bool scalar type. Use JS constructor functions (`String`, `Boolean`) then: + +```ts +@ObjectType() +class SampleObject { + @Field(type => String, { nullable: true }) + // TS reflected type is `Object` :( + get optionalInfo(): string | undefined { + if (Math.random() > 0.5) { + return "Gotcha!"; + } + } +} +``` + +## Custom Scalars + +TypeGraphQL also supports custom scalar types! + +First of all, we need to create our own `GraphQLScalarType` instance or import a scalar type from a 3rd-party npm library. For example, Mongo's ObjectId: + +```ts +import { GraphQLScalarType, Kind } from "graphql"; +import { ObjectId } from "mongodb"; + +export const ObjectIdScalar = new GraphQLScalarType({ + name: "ObjectId", + description: "Mongo object id scalar type", + serialize(value: unknown): string { + // Check type of value + if (!(value instanceof ObjectId)) { + throw new Error("ObjectIdScalar can only serialize ObjectId values"); + } + return value.toHexString(); // Value sent to client + }, + parseValue(value: unknown): ObjectId { + // Check type of value + if (typeof value !== "string") { + throw new Error("ObjectIdScalar can only parse string values"); + } + return new ObjectId(value); // Value from client input variables + }, + parseLiteral(ast): ObjectId { + // Check type of value + if (ast.kind !== Kind.STRING) { + throw new Error("ObjectIdScalar can only parse string values"); + } + return new ObjectId(ast.value); // Value from client query + }, +}); +``` + +Then we can just use it in our field decorators: + +```ts +// Import earlier created const +import { ObjectIdScalar } from "../my-scalars/ObjectId"; + +@ObjectType() +class User { + @Field(type => ObjectIdScalar) // Explicitly use it + readonly id: ObjectId; + + @Field() + name: string; + + @Field() + isOld: boolean; +} +``` + +Optionally, we can declare the association between the reflected property type and our scalars to automatically map them (no need for explicit type annotation!): + +```ts +@ObjectType() +class User { + @Field() // Magic goes here - no type annotation for custom scalar + readonly id: ObjectId; +} +``` + +All we need to do is register the association map in the `buildSchema` options: + +```ts +import { ObjectId } from "mongodb"; +import { ObjectIdScalar } from "../my-scalars/ObjectId"; +import { buildSchema } from "type-graphql"; + +const schema = await buildSchema({ + resolvers, + scalarsMap: [{ type: ObjectId, scalar: ObjectIdScalar }], +}); +``` + +However, we must be aware that this will only work when the TypeScript reflection mechanism can handle it. So our class property type must be a `class`, not an enum, union or interface. + +## Date Scalars + +TypeGraphQL provides built-in scalars for the `Date` type. There are two versions of this scalar: + +- ISO-formatted string: `"2023-05-19T21:04:39.573Z"` +- timestamp-based number: `1518037458374` + +They are exported from the `type-graphql` package as `GraphQLISODateTime` and `GraphQLTimestamp` but comes from `graphql-scalars` npm package. + +By default, TypeGraphQL uses the ISO date format, however we can change it to timestamp format using the mentioned above `scalarsMap` option of `buildSchema` configuration: + +```ts +import { buildSchema, GraphQLTimestamp } from "type-graphql"; + +const schema = await buildSchema({ + resolvers, + scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }], +}); +``` + +There's no need then to explicitly declare the field type: + +```ts +@ObjectType() +class User { + @Field() + registrationDate: Date; +} +``` + +We can of course use any other `Date` scalar from `graphql-scalars` or any other npm package. diff --git a/website/versioned_docs/version-2.0.0-beta.3/subscriptions.md b/website/versioned_docs/version-2.0.0-beta.3/subscriptions.md new file mode 100644 index 000000000..b97395791 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/subscriptions.md @@ -0,0 +1,191 @@ +--- +title: Subscriptions +id: version-2.0.0-beta.3-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-subscriptions](https://github.com/apollographql/graphql-subscriptions) package created by [Apollo GraphQL](https://www.apollographql.com). + +## 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, payload, 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 `AsyncIterator`. Example using Prisma client 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 [`withFilter` function](https://github.com/apollographql/graphql-subscriptions#filters) from the `graphql-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; + } +} +``` + +We use the `@PubSub()` decorator to inject the `pubsub` into our method params. +There we can trigger the topics and send the payload to all topic subscribers. + +```ts +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 }; + await pubSub.publish("NOTIFICATIONS", payload); + return true; + } +} +``` + +For easier testability (mocking/stubbing), we can also inject the `publish` method by itself bound to a selected topic. +This is done by using the `@PubSub("TOPIC_NAME")` decorator and the `Publisher` type: + +```ts +class SampleResolver { + // ... + @Mutation(returns => Boolean) + async addNewComment( + @Arg("comment") input: CommentInput, + @PubSub("NOTIFICATIONS") publish: Publisher, + ) { + const comment = this.commentsService.createNew(input); + await this.commentsRepository.save(comment); + // Trigger subscriptions topics + await publish({ message: input.content }); + return true; + } +} +``` + +And that's it! Now all subscriptions attached to the `NOTIFICATIONS` topic will be triggered when performing the `addNewComment` mutation. + +## Using a custom PubSub system + +By default, TypeGraphQL uses a simple `PubSub` system from `graphql-subscriptions` which is based on EventEmitter. +This solution has a big drawback in that it will work correctly only when we have a single instance (process) of our Node.js app. + +For better scalability we'll want to use one of the [`PubSub implementations`](https://github.com/apollographql/graphql-subscriptions#pubsub-implementations) backed by an external store like Redis with the [`graphql-redis-subscriptions`](https://github.com/davidyaha/graphql-redis-subscriptions) package. + +All we need to do is create an instance of PubSub according to the package instructions and then provide it to the TypeGraphQL `buildSchema` options: + +```ts +const myRedisPubSub = getConfiguredRedisPubSub(); + +const schema = await buildSchema({ + resolvers: [ExampleResolver], + pubSub: myRedisPubSub, +}); +``` + +## 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-beta.3/examples/simple-subscriptions). + +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-beta.3/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-beta.3/types-and-fields.md b/website/versioned_docs/version-2.0.0-beta.3/types-and-fields.md new file mode 100644 index 000000000..541da096b --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/types-and-fields.md @@ -0,0 +1,140 @@ +--- +title: Types and Fields +id: version-2.0.0-beta.3-types-and-fields +original_id: types-and-fields +--- + +The main idea of TypeGraphQL is to automatically create GraphQL schema definitions from TypeScript classes. To avoid the need for schema definition files and interfaces describing the schema, we use decorators and a bit of reflection magic. + +Let's start by defining our example TypeScript class which represents our `Recipe` model with fields for storing the recipe data: + +```ts +class Recipe { + id: string; + title: string; + ratings: Rate[]; + averageRating?: number; +} +``` + +The first thing we must do is decorate the class with the `@ObjectType` decorator. It marks the class as the `type` known from the GraphQL SDL or `GraphQLObjectType` from `graphql-js`: + +```ts +@ObjectType() +class Recipe { + id: string; + title: string; + ratings: Rate[]; + averageRating: number; +} +``` + +Then we declare which class properties should be mapped to the GraphQL fields. +To do this, we use the `@Field` decorator, which is also used to collect metadata from the TypeScript reflection system: + +```ts +@ObjectType() +class Recipe { + @Field() + id: string; + + @Field() + title: string; + + @Field() + ratings: Rate[]; + + @Field() + averageRating: number; +} +``` + +For simple types (like `string` or `boolean`) this is all that's needed but due to a limitation in TypeScript's reflection, we need to provide info about generic types (like `Array` or `Promise`). So to declare the `Rate[]` type, we have to use the explicit `[ ]` syntax for array types - `@Field(type => [Rate])`. +For nested arrays, we just use the explicit `[ ]` notation to determine the depth of the array, e.g. `@Field(type => [[Int]])` would tell the compiler we expect an integer array of depth 2. + +Why use function syntax and not a simple `{ type: Rate }` config object? Because, by using function syntax we solve the problem of circular dependencies (e.g. Post <--> User), so it was adopted as a convention. You can use the shorthand syntax `@Field(() => Rate)` if you want to save some keystrokes but it might be less readable for others. + +By default, all fields are non nullable, just like properties in TypeScript. However, you can change that behavior by providing `nullableByDefault: true` option in `buildSchema` settings, described in [bootstrap guide](./bootstrap.md). + +So for nullable properties like `averageRating` which might not be defined when a recipe has no ratings yet, we mark the class property as optional with a `?:` operator and also have to pass the `{ nullable: true }` decorator parameter. We should be aware that when we declare our type as a nullable union (e.g. `string | null`), we need to explicitly provide the type to the `@Field` decorator. + +In the case of lists, we may also need to define their nullability in a more detailed form. The basic `{ nullable: true | false }` setting only applies to the whole list (`[Item!]` or `[Item!]!`), so if we need a sparse array, we can control the list items' nullability via `nullable: "items"` (for `[Item]!`) or `nullable: "itemsAndList"` (for the `[Item]`) option. Be aware that setting `nullableByDefault: true` option will also apply to lists, so it will produce `[Item]` type, just like with `nullable: "itemsAndList"`. + +For nested lists, those options apply to the whole depth of the array: `@Field(() => [[Item]]` would by default produce `[[Item!]!]!`, setting `nullable: "itemsAndList"` would produce `[[Item]]` while `nullable: "items"` would produce `[[Item]]!` + +In the config object we can also provide the `description` and `deprecationReason` properties for GraphQL schema purposes. + +So after these changes our example class would look like this: + +```ts +@ObjectType({ description: "The recipe model" }) +class Recipe { + @Field(type => ID) + id: string; + + @Field({ description: "The title of the recipe" }) + title: string; + + @Field(type => [Rate]) + ratings: Rate[]; + + @Field({ nullable: true }) + averageRating?: number; +} +``` + +Which will result in generating the following part of the GraphQL schema in SDL: + +```graphql +type Recipe { + id: ID! + title: String! + ratings: [Rate!]! + averageRating: Float +} +``` + +Similarly, the `Rate` type class would look like this: + +```ts +@ObjectType() +class Rate { + @Field(type => Int) + value: number; + + @Field() + date: Date; + + user: User; +} +``` + +which results in this equivalent of the GraphQL SDL: + +```graphql +type Rate { + value: Int! + date: Date! +} +``` + +As we can see, for the `id` property of `Recipe` we passed `type => ID` and for the `value` field of `Rate` we passed `type => Int`. This way we can overwrite the inferred type from the reflection metadata. We can read more about the ID and Int scalars in [the scalars docs](./scalars.md). There is also a section about the built-in `Date` scalar. + +Also the `user` property doesn't have a `@Field()` decorator - this way we can hide some properties of our data model. In this case, we need to store the `user` field of the `Rate` object to the database in order to prevent multiple rates, but we don't want to make it publicly accessible. + +Note that if a field of an object type is purely calculable (e.g. `averageRating` from `ratings` array) and we don't want to pollute the class signature, we can omit it and just implement the field resolver (described in [resolvers doc](./resolvers.md)). + +Be aware that **defining constructors is strictly forbidden** and we shouldn't use them there, as TypeGraphQL creates instances of object type classes under the hood by itself. + +In some case we may want to expose our classes or properties under a different types or fields name. +To accomplish this, we can use the `name` parameter or `name` property of decorator's options, e.g.: + +```ts +@ObjectType("ExternalTypeName") +class InternalClassName { + @Field({ name: "externalFieldName" }) + internalPropertyName: string; +} +``` + +However, be aware that renaming fields works only for output types like object type or interface type. It's due to a fact that input fields has no resolvers that could translate one field value into another property value. diff --git a/website/versioned_docs/version-2.0.0-beta.3/unions.md b/website/versioned_docs/version-2.0.0-beta.3/unions.md new file mode 100644 index 000000000..dcab4790a --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/unions.md @@ -0,0 +1,109 @@ +--- +title: Unions +id: version-2.0.0-beta.3-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-beta.3/examples/enums-and-unions). diff --git a/website/versioned_docs/version-2.0.0-beta.3/validation.md b/website/versioned_docs/version-2.0.0-beta.3/validation.md new file mode 100644 index 000000000..674115459 --- /dev/null +++ b/website/versioned_docs/version-2.0.0-beta.3/validation.md @@ -0,0 +1,237 @@ +--- +title: Argument and Input validation +sidebar_label: Validation +id: version-2.0.0-beta.3-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-beta.3/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 as `validate` option in `buildSchema`. +It receives two parameters: + +- `argValue` which is the injected value of `@Arg()` or `@Args()` +- `argType` which is a runtime type information (e.g. `String` or `RecipeInput`). + +The `validateFn` option 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. + +Example using [decorators library for Joi validators (`joiful`)](https://github.com/joiful-ts/joiful): + +```ts +const schema = await buildSchema({ + // ... + validate: argValue => { + // Call joiful validate + const { error } = joiful.validate(argValue); + if (error) { + // Throw error on failed validation + throw error; + } + }, +}); +``` + +> 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-beta.3/examples/custom-validation). diff --git a/website/versioned_sidebars/version-2.0.0-beta.3-sidebars.json b/website/versioned_sidebars/version-2.0.0-beta.3-sidebars.json new file mode 100644 index 000000000..d11fc8616 --- /dev/null +++ b/website/versioned_sidebars/version-2.0.0-beta.3-sidebars.json @@ -0,0 +1,53 @@ +{ + "version-2.0.0-beta.3-docs": { + "Introduction": [ + "version-2.0.0-beta.3-introduction" + ], + "Beginner guides": [ + "version-2.0.0-beta.3-installation", + "version-2.0.0-beta.3-getting-started", + "version-2.0.0-beta.3-types-and-fields", + "version-2.0.0-beta.3-resolvers", + "version-2.0.0-beta.3-bootstrap", + "version-2.0.0-beta.3-esm" + ], + "Advanced guides": [ + "version-2.0.0-beta.3-scalars", + "version-2.0.0-beta.3-enums", + "version-2.0.0-beta.3-unions", + "version-2.0.0-beta.3-interfaces", + "version-2.0.0-beta.3-subscriptions", + "version-2.0.0-beta.3-directives", + "version-2.0.0-beta.3-extensions" + ], + "Features": [ + "version-2.0.0-beta.3-dependency-injection", + "version-2.0.0-beta.3-authorization", + "version-2.0.0-beta.3-validation", + "version-2.0.0-beta.3-inheritance", + "version-2.0.0-beta.3-generic-types", + "version-2.0.0-beta.3-middlewares", + "version-2.0.0-beta.3-custom-decorators", + "version-2.0.0-beta.3-complexity" + ], + "Integrations": [ + "version-2.0.0-beta.3-prisma", + "version-2.0.0-beta.3-nestjs" + ], + "Others": [ + "version-2.0.0-beta.3-emit-schema", + "version-2.0.0-beta.3-performance", + "version-2.0.0-beta.3-browser-usage" + ] + }, + "version-2.0.0-beta.3-examples": { + "Examples": [ + "version-2.0.0-beta.3-examples" + ] + }, + "version-2.0.0-beta.3-others": { + "Others": [ + "version-2.0.0-beta.3-faq" + ] + } +} diff --git a/website/versions.json b/website/versions.json index d7c613e5d..5c57d7721 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "2.0.0-beta.3", "1.2.0-rc.1", "1.1.1", "1.1.0",