From 02163217b160a7fc1ef737a4cab6e82dbc9fbc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lytek?= Date: Mon, 29 Jun 2020 20:06:06 +0200 Subject: [PATCH] release: 1.0.0-rc.3 --- CHANGELOG.md | 3 +- .../version-1.0.0-rc.3/bootstrap.md | 144 ++++++++ .../version-1.0.0-rc.3/browser-usage.md | 40 ++ .../version-1.0.0-rc.3/examples.md | 48 +++ .../version-1.0.0-rc.3/resolvers.md | 348 ++++++++++++++++++ .../version-1.0.0-rc.3/types-and-fields.md | 127 +++++++ website/versions.json | 1 + 7 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 website/versioned_docs/version-1.0.0-rc.3/bootstrap.md create mode 100644 website/versioned_docs/version-1.0.0-rc.3/browser-usage.md create mode 100644 website/versioned_docs/version-1.0.0-rc.3/examples.md create mode 100644 website/versioned_docs/version-1.0.0-rc.3/resolvers.md create mode 100644 website/versioned_docs/version-1.0.0-rc.3/types-and-fields.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d269938..46cb9b733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog and release notes -## Unreleased + +## v1.0.0-rc.3 ### Features - **Breaking Change**: remove legacy array inference - now explicit array syntax (`[Item]`) is required - **Breaking Change**: update `graphql-js` peer dependency to `^15.1.0` diff --git a/website/versioned_docs/version-1.0.0-rc.3/bootstrap.md b/website/versioned_docs/version-1.0.0-rc.3/bootstrap.md new file mode 100644 index 000000000..63de071ce --- /dev/null +++ b/website/versioned_docs/version-1.0.0-rc.3/bootstrap.md @@ -0,0 +1,144 @@ +--- +title: Bootstrapping +id: version-1.0.0-rc.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 can be an array of resolver classes: + +```typescript +import { FirstResolver, SecondResolver } from "../app/src/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`: + +```typescript +import { FirstResolver, SecondResolver } from "../app/src/resolvers"; +import { FirstObject } from "../app/src/types"; +// ... +const schema = await buildSchema({ + resolvers: [FirstResolver, SecondResolver], + // here 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: + +```typescript +// resolvers.ts +export const resolvers = [FirstResolver, SecondResolver] as const; + +// schema.ts +import { resolvers } from "./resolvers"; + +const schema = await buildSchema({ resolvers }); +``` + +However, when there are several resolver classes, manual imports can be cumbersome. +So we can also provide an array of paths to resolver module files instead, which can include globs: + +```typescript +const schema = await buildSchema({ + resolvers: [__dirname + "/modules/**/*.resolver.{ts,js}", __dirname + "/resolvers/**/*.{ts,js}"], +}); +``` + +> Be aware that in case of providing paths to resolvers files, TypeGraphQL will emit all the operations and types that are imported in the resolvers files or their dependencies. + +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: + +```typescript +import { buildSchema } from "type-graphql"; + +async function bootstrap() { + const schema = await buildSchema({ + resolvers: [__dirname + "/**/*.resolver.{ts,js}"], + }); + + // other initialization code, like creating http server +} + +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/prisma/graphql-yoga) or [`apollo-server`](https://github.com/apollographql/apollo-server). Here is an example using [`apollo-server`](https://github.com/apollographql/apollo-server): + +```typescript +import { ApolloServer } from "apollo-server"; + +const PORT = process.env.PORT || 4000; + +async function bootstrap() { + // ... Building schema here + + // Create the GraphQL server + const server = new ApolloServer({ + schema, + playground: true, + }); + + // Start the server + const { url } = await server.listen(PORT); + console.log(`Server is running, GraphQL Playground available 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://github.com/apollographql/graphql-tools): + +```typescript +import { makeExecutableSchema } from "graphql-tools"; + +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): + +```typescript +import { withClientState } from "apollo-link-state"; + +const { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ + resolvers: [FirstResolver, SecondResolver], +}); + +const stateLink = withClientState({ + // ...other options like `cache` + typeDefs, + resolvers, +}); + +// ...the rest of `ApolloClient` initialization code +``` + +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-1.0.0-rc.3/browser-usage.md b/website/versioned_docs/version-1.0.0-rc.3/browser-usage.md new file mode 100644 index 000000000..d3b1025de --- /dev/null +++ b/website/versioned_docs/version-1.0.0-rc.3/browser-usage.md @@ -0,0 +1,40 @@ +--- +title: Browser usage +id: version-1.0.0-rc.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 = { + // ... the rest of the webpack config + plugins: [ + // ... here are any other existing plugins that we already have + 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-1.0.0-rc.3/examples.md b/website/versioned_docs/version-1.0.0-rc.3/examples.md new file mode 100644 index 000000000..3cdad7e14 --- /dev/null +++ b/website/versioned_docs/version-1.0.0-rc.3/examples.md @@ -0,0 +1,48 @@ +--- +title: Examples +sidebar_label: List of examples +id: version-1.0.0-rc.3-examples +original_id: examples +--- + +On the [GitHub repository](https://github.com/MichalLytek/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/MichalLytek/type-graphql/tree/master/examples/simple-usage) + +## Advanced + +- [Enums and unions](https://github.com/MichalLytek/type-graphql/tree/master/examples/enums-and-unions) +- [Subscriptions (simple)](https://github.com/MichalLytek/type-graphql/tree/master/examples/simple-subscriptions) +- [Subscriptions (using Redis)](https://github.com/MichalLytek/type-graphql/tree/master/examples/redis-subscriptions) +- [Interfaces](https://github.com/MichalLytek/type-graphql/tree/master/examples/interfaces-inheritance) +- [Extensions (metadata)](https://github.com/MichalLytek/type-graphql/tree/master/examples/extensions) + +## Features usage + +- [Dependency injection (IoC container)](https://github.com/MichalLytek/type-graphql/tree/master/examples/using-container) + - [Scoped containers](https://github.com/MichalLytek/type-graphql/tree/master/examples/using-scoped-container) +- [Authorization](https://github.com/MichalLytek/type-graphql/tree/master/examples/authorization) +- [Validation](https://github.com/MichalLytek/type-graphql/tree/master/examples/automatic-validation) +- [Types inheritance](https://github.com/MichalLytek/type-graphql/tree/master/examples/interfaces-inheritance) +- [Resolvers inheritance](https://github.com/MichalLytek/type-graphql/tree/master/examples/resolvers-inheritance) +- [Generic types](https://github.com/MichalLytek/type-graphql/tree/master/examples/generic-types) +- [Mixin classes](https://github.com/MichalLytek/type-graphql/tree/master/examples/mixin-classes) +- [Middlewares and Custom Decorators](https://github.com/MichalLytek/type-graphql/tree/master/examples/middlewares-custom-decorators) +- [Query complexity](https://github.com/MichalLytek/type-graphql/tree/master/examples/query-complexity) + +## 3rd party libs integration + +- [TypeORM (manual, synchronous) \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typeorm-basic-usage) +- [TypeORM (automatic, lazy relations) \*](https://github.com/MichalLytek/type-graphql/tree/master/examples/typeorm-lazy-relations) +- [Typegoose](https://github.com/MichalLytek/type-graphql/tree/master/examples/typegoose) +- [Apollo federation](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-federation) +- [Apollo Engine (Apollo Cache Control) \*\*](https://github.com/MichalLytek/type-graphql/tree/master/examples/apollo-engine) +- [Apollo client state](https://github.com/MichalLytek/type-graphql/tree/master/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-1.0.0-rc.3/resolvers.md b/website/versioned_docs/version-1.0.0-rc.3/resolvers.md new file mode 100644 index 000000000..6b296bd36 --- /dev/null +++ b/website/versioned_docs/version-1.0.0-rc.3/resolvers.md @@ -0,0 +1,348 @@ +--- +title: Resolvers +id: version-1.0.0-rc.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: + +```typescript +@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. + +```typescript +@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: + +```typescript +@Resolver() +class RecipeResolver { + private recipesCollection: Recipe[] = []; + + async recipes() { + // fake async in this example + 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. + +```typescript +@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. + +```typescript +@Resolver() +class RecipeResolver { + // ... + @Query(returns => [Recipe]) + async recipes( + @Arg("title", { nullable: true }) title?: string, + @Arg("servings", { defaultValue: 2 }) servings: number, + ): 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. + +```typescript +@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 and making the field nullable. + +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. + +```typescript +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. + +```typescript +@Resolver() +class RecipeResolver { + // ... + @Query(returns => [Recipe]) + async recipes(@Args() { title, startIndex, endIndex }: GetRecipesArgs) { + // sample 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: + +```typescript +@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: + +```typescript +@InputType() +class AddRecipeInput implements Partial {} +``` + +We then declare any input fields we need, using the `@Field()` decorator: + +```typescript +@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: + +```typescript +@Resolver() +class RecipeResolver { + // ... + @Mutation() + addRecipe(@Arg("data") newRecipeData: AddRecipeInput, @Ctx() ctx: Context): Recipe { + // sample 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: + +```typescript +@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. + +```typescript +@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. + +```typescript +@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). + +```typescript +@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: + +```typescript +@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: + +```typescript +@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: + +```typescript +import { Repository } from "typeorm"; + +@Resolver(of => Recipe) +class RecipeResolver implements ResolverInterface { + constructor( + private userRepository: Repository, // dependency injection + ) {} + + @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/master/examples). diff --git a/website/versioned_docs/version-1.0.0-rc.3/types-and-fields.md b/website/versioned_docs/version-1.0.0-rc.3/types-and-fields.md new file mode 100644 index 000000000..df038a123 --- /dev/null +++ b/website/versioned_docs/version-1.0.0-rc.3/types-and-fields.md @@ -0,0 +1,127 @@ +--- +title: Types and Fields +id: version-1.0.0-rc.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: + +```typescript +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`: + +```typescript +@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: + +```typescript +@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 defaut 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: + +```typescript +@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: + +```typescript +@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. diff --git a/website/versions.json b/website/versions.json index 81ee0ec32..1c76bcca7 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "1.0.0-rc.3", "1.0.0-rc.2", "1.0.0-rc.1", "0.17.6",