diff --git a/CHANGELOG.md b/CHANGELOG.md index db1e93408..9983672c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog and release notes -## Unreleased + + +## v0.17.4 ## Features - add support for creating custom parameter decorators (#329) - allow to provide custom `subscribe` function in `@Subscription` decorator (#328) diff --git a/package-lock.json b/package-lock.json index 63320a719..01c9e645f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "type-graphql", - "version": "0.17.3", + "version": "0.17.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c89378ed8..513f8bd5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "type-graphql", - "version": "0.17.3", + "version": "0.17.4", "author": { "name": "Michał Lytek", "url": "https://github.com/19majkel94" diff --git a/website/i18n/en.json b/website/i18n/en.json index 43186e8fc..9eb05c4ba 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -297,6 +297,25 @@ }, "version-0.17.2-unions": { "title": "Unions" + }, + "version-0.17.4-custom-decorators": { + "title": "Custom decorators" + }, + "version-0.17.4-examples": { + "title": "Examples", + "sidebar_label": "List of examples" + }, + "version-0.17.4-interfaces": { + "title": "Interfaces" + }, + "version-0.17.4-middlewares": { + "title": "Middleware and guards" + }, + "version-0.17.4-subscriptions": { + "title": "Subscriptions" + }, + "version-0.17.4-unions": { + "title": "Unions" } }, "links": { diff --git a/website/versioned_docs/version-0.17.4/custom-decorators.md b/website/versioned_docs/version-0.17.4/custom-decorators.md new file mode 100644 index 000000000..ab80ec6a2 --- /dev/null +++ b/website/versioned_docs/version-0.17.4/custom-decorators.md @@ -0,0 +1,106 @@ +--- +title: Custom decorators +id: version-0.17.4-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: + +```typescript +export function ValidateArgs(schema: JoiSchema) { + return createMethodDecorator(async ({ args }, next) => { + // here place your 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: + +```typescript +@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: + +```typescript +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): + +```typescript +function Fields(level = 1): ParameterDecorator { + return createParamDecorator(({ info }) => { + const fieldsMap: FieldsMap = {}; + // calculate an object with info about requested fields + // based on GraphQL `info` parameter of the resolver and the level parameter + return fieldsMap; + } +} +``` + +Then we can use our custom param decorators in the resolvers just like the built-in decorators: + +```typescript +@Resolver() +export class RecipeResolver { + constructor(private readonly recipesRepository: Repository) {} + + @Authorized() + @Mutation(returns => Recipe) + async addRecipe( + @Args() recipeData: AddRecipeInput, + // here we place our custom decorator + // just like the built-in one + @CurrentUser() currentUser: User, + ) { + const recipe: Recipe = { + ...recipeData, + // and use the data returned from custom decorator in our resolver code + author: currentUser, + }; + await this.recipesRepository.save(recipe); + return recipe; + } + + @Query(returns => Recipe, { nullable: true }) + async recipe( + @Arg("id") id: string, + // our 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/19majkel94/type-graphql/tree/v0.17.4/examples/middlewares-custom-decorators). diff --git a/website/versioned_docs/version-0.17.4/examples.md b/website/versioned_docs/version-0.17.4/examples.md new file mode 100644 index 000000000..20727d63e --- /dev/null +++ b/website/versioned_docs/version-0.17.4/examples.md @@ -0,0 +1,44 @@ +--- +title: Examples +sidebar_label: List of examples +id: version-0.17.4-examples +original_id: examples +--- + +On the [GitHub repository](https://github.com/19majkel94/type-graphql) there are a few simple examples of how to use different TypeGraphQL features and how well they integrate with 3rd party libraries. + +All examples have an `examples.gql` file with sample queries/mutations/subscriptions that we can execute. + +## Basics + +- [Simple usage of fields, basic types and resolvers](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/simple-usage) + +## Advanced + +- [Enums and unions](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/enums-and-unions) +- [Subscriptions (simple)](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/simple-subscriptions) +- [Subscriptions (using Redis)](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/redis-subscriptions) +- [Interfaces](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/interfaces-inheritance) + +## Features usage + +- [Dependency injection (IoC container)](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/using-container) + - [scoped container](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/using-scoped-container) +- [Authorization](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/authorization) +- [Validation](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/automatic-validation) +- [Types inheritance](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/interfaces-inheritance) +- [Resolvers inheritance](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/resolvers-inheritance) +- [Generic types](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/generic-types) +- [Middlewares and Custom Decorators](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/middlewares-custom-decorators) + +## 3rd party libs integration + +- [TypeORM (manual, synchronous) \*](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/typeorm-basic-usage) +- [TypeORM (automatic, lazy relations) \*](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/typeorm-lazy-relations) +- [Typegoose](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/typegoose) +- [Apollo Engine (Apollo Cache Control) \*\*](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/apollo-engine) +- [Apollo client state](https://github.com/19majkel94/type-graphql/tree/v0.17.4/examples/apollo-client) + +_\* Note that we need to edit the TypeORM example's `index.ts` with the credentials of our local database_ + +_\*\* Note that we need to provide an `APOLLO_ENGINE_API_KEY` env variable with our own API key_ diff --git a/website/versioned_docs/version-0.17.4/interfaces.md b/website/versioned_docs/version-0.17.4/interfaces.md new file mode 100644 index 000000000..af5680d08 --- /dev/null +++ b/website/versioned_docs/version-0.17.4/interfaces.md @@ -0,0 +1,74 @@ +--- +title: Interfaces +id: version-0.17.4-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). + +## Usage + +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 - it can't be "newed" but it can be implemented by the class - and it just won't prevent developers from implementing a method or initializing a field. So, as long as we treat it like an interface, we can safely use it. + +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: + +```typescript +@InterfaceType() +abstract class IPerson { + @Field(type => ID) + id: string; + + @Field() + name: string; + + @Field(type => Int) + age: number; +} +``` + +We can then we use this "interface" in the object type class definition: + +```typescript +@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] })`. + +We can also 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. + +## 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.: + +```typescript +@InterfaceType({ + resolveType: value => { + if ("grades" in value) { + return "Student"; // schema name of the type as a string + } + return Person; // or the 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/19majkel94/type-graphql/tree/v0.17.4/examples/interfaces-inheritance). diff --git a/website/versioned_docs/version-0.17.4/middlewares.md b/website/versioned_docs/version-0.17.4/middlewares.md new file mode 100644 index 000000000..37dff67a6 --- /dev/null +++ b/website/versioned_docs/version-0.17.4/middlewares.md @@ -0,0 +1,189 @@ +--- +title: Middleware and guards +id: version-0.17.4-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: + +```typescript +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: + +```typescript +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: + +```typescript +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. + +```typescript +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. + +```typescript +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: + +```typescript +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: + +```typescript +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): + +```typescript +@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). + +```typescript +@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: + +```typescript +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/19majkel94/type-graphql/tree/v0.17.4/examples/middlewares-custom-decorators). diff --git a/website/versioned_docs/version-0.17.4/subscriptions.md b/website/versioned_docs/version-0.17.4/subscriptions.md new file mode 100644 index 000000000..2e3902865 --- /dev/null +++ b/website/versioned_docs/version-0.17.4/subscriptions.md @@ -0,0 +1,204 @@ +--- +title: Subscriptions +id: version-0.17.4-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. + +```typescript +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. + +```typescript +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. + +```typescript +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 the `subscribe` option which should be a function that returns an `AsyncIterator`. Example using Prisma client subscription feature: + +```typescript +class SampleResolver { + // ... + @Subscription({ + subscribe: ({ args, context }) => { + 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. + +```typescript +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: + +```typescript +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. + +```typescript +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); + // here we can 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: + +```typescript +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); + // here we can 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 `grapqhl-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: + +```typescript +const myRedisPubSub = getConfiguredRedisPubSub(); + +const schema = await buildSchema({ + resolvers: [__dirname + "/**/*.resolver.ts"], + 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. + +Fortunately, to make subscriptions work, we don't need to manually provide a transport layer that doesn't have constraints of HTTP and can do a push-based communication (WebSockets). +The `apollo-server` package has built-in subscriptions support using websockets, so it works out of the box without any changes to our bootstrap config. However, if we want, we can provide the `subscriptions` property of the config object: + +```typescript +// Create GraphQL server +const server = new ApolloServer({ + schema, + subscriptions: { + path: "/subscriptions", + // other options and hooks, like `onConnect` + }, +}); +``` + +And it's done! We have a working GraphQL subscription server on `/subscriptions`, along with the normal HTTP GraphQL server. + +## Examples + +See how subscriptions work in a [simple example](https://github.com/19majkel94/type-graphql/tree/v0.17.4/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/19majkel94/type-graphql/tree/v0.17.4/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-0.17.4/unions.md b/website/versioned_docs/version-0.17.4/unions.md new file mode 100644 index 000000000..3672b1bb7 --- /dev/null +++ b/website/versioned_docs/version-0.17.4/unions.md @@ -0,0 +1,109 @@ +--- +title: Unions +id: version-0.17.4-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: + +```typescript +@ObjectType() +class Movie { + @Field() + name: string; + + @Field() + rating: number; +} +``` + +```typescript +@ObjectType() +class Actor { + @Field() + name: string; + + @Field(type => Int) + age: number; +} +``` + +Now let's create a union type from the object types above: + +```typescript +import { createUnionType } from "type-graphql"; + +const SearchResultUnion = createUnionType({ + name: "SearchResult", // the name of the GraphQL union + types: [Movie, Actor], // array of object types classes +}); +``` + +Now we can use the union type in the query. +Notice, that due to TypeScript's reflection limitation, we have to explicitly use the `SearchResultUnion` value in the `@Query` decorator return type annotation. +For TypeScript compile-time type safety we can also use `typeof SearchResultUnion` which is equal to type `Movie | Actor`. + +```typescript +@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.: + +```typescript +const SearchResultUnion = createUnionType({ + name: "SearchResult", + types: [Movie, Actor], + // our implementation of detecting returned object type + resolveType: value => { + if ("rating" in value) { + return Movie; // we can 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/19majkel94/type-graphql/tree/v0.17.4/examples/enums-and-unions). diff --git a/website/versioned_sidebars/version-0.17.4-sidebars.json b/website/versioned_sidebars/version-0.17.4-sidebars.json new file mode 100644 index 000000000..a24e3fac0 --- /dev/null +++ b/website/versioned_sidebars/version-0.17.4-sidebars.json @@ -0,0 +1,45 @@ +{ + "version-0.17.4-docs": { + "Introduction": [ + "version-0.17.4-introduction" + ], + "Beginner guides": [ + "version-0.17.4-installation", + "version-0.17.4-getting-started", + "version-0.17.4-types-and-fields", + "version-0.17.4-resolvers", + "version-0.17.4-bootstrap" + ], + "Advanced guides": [ + "version-0.17.4-scalars", + "version-0.17.4-enums", + "version-0.17.4-unions", + "version-0.17.4-interfaces", + "version-0.17.4-subscriptions" + ], + "Features": [ + "version-0.17.4-dependency-injection", + "version-0.17.4-authorization", + "version-0.17.4-validation", + "version-0.17.4-inheritance", + "version-0.17.4-generic-types", + "version-0.17.4-middlewares", + "version-0.17.4-custom-decorators", + "version-0.17.4-complexity" + ], + "Others": [ + "version-0.17.4-emit-schema", + "version-0.17.4-browser-usage" + ] + }, + "version-0.17.4-examples": { + "Examples": [ + "version-0.17.4-examples" + ] + }, + "version-0.17.4-others": { + "Others": [ + "version-0.17.4-faq" + ] + } +} diff --git a/website/versions.json b/website/versions.json index 867a7d28e..cd8729588 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "0.17.4", "0.17.3", "0.17.2", "0.17.1",