diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f8b2f41..c60077bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Changelog and release notes - +## Unreleased +## Features +- add support for creating custom parameter decorators (#329) + ## v0.17.3 ### Features - update packages `semver` to `^6.0.0` and `graphql-subscriptions` to `^1.1.0` diff --git a/dev.js b/dev.js index f064da5c2..375124708 100644 --- a/dev.js +++ b/dev.js @@ -5,7 +5,7 @@ require("ts-node/register/transpile-only"); // require("./examples/enums-and-unions/index.ts"); // require("./examples/generic-types/index.ts"); // require("./examples/interfaces-inheritance/index.ts"); -// require("./examples/middlewares/index.ts"); +// require("./examples/middlewares-custom-decorators/index.ts"); // require("./examples/query-complexity/index.ts"); // require("./examples/redis-subscriptions/index.ts"); // require("./examples/resolvers-inheritance/index.ts"); diff --git a/docs/custom-decorators.md b/docs/custom-decorators.md new file mode 100644 index 000000000..1e93e771d --- /dev/null +++ b/docs/custom-decorators.md @@ -0,0 +1,104 @@ +--- +title: 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/master/examples/middlewares-custom-decorators). diff --git a/docs/examples.md b/docs/examples.md index 2c3c76784..ea5856dcd 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -27,7 +27,7 @@ All examples have an `examples.gql` file with sample queries/mutations/subscript - [Types inheritance](https://github.com/19majkel94/type-graphql/tree/master/examples/interfaces-inheritance) - [Resolvers inheritance](https://github.com/19majkel94/type-graphql/tree/master/examples/resolvers-inheritance) - [Generic types](https://github.com/19majkel94/type-graphql/tree/master/examples/generic-types) -- [Middlewares](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares) +- [Middlewares and Custom Decorators](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares-custom-decorators) ## 3rd party libs integration diff --git a/docs/middlewares.md b/docs/middlewares.md index f89c7a9be..0659c5859 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -180,28 +180,8 @@ const schema = await buildSchema({ ### Custom Decorators -If we want to have a more descriptive and declarative API, we can also create custom decorators. They work in the same way as the reusable middleware function, however, in this case we need to return the `UseMiddleware` decorator function: +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). -```typescript -export function ValidateArgs(schema: Schema) { - return UseMiddleware(async ({ args }, next) => { - // here place your validation logic, e.g. 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 the id: +## Example -```typescript -@Resolver() -export class RecipeResolver { - @Query() - @ValidateArgs(MyArgsSchema) // custom decorator - @UseMiddleware(ResolveTime) // explicit middleware - randomValue(@Args() { scale }: MyArgs): number { - return Math.random() * scale; - } -} -``` +See how different kinds of middlewares work in the [middlewares and custom decorators example](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares-custom-decorators). diff --git a/examples/README.md b/examples/README.md index 93720b3c8..2592be4f0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,7 +27,7 @@ So if you are looking for examples that are compatible with the version you use, - [Types inheritance](./interfaces-inheritance) - [Resolvers inheritance](./resolvers-inheritance) - [Generic types](./generic-types) -- [Middlewares](./middlewares) +- [Middlewares and custom decorators](./middlewares-custom-decorators) ## 3rd party libs integration diff --git a/examples/middlewares-custom-decorators/context.ts b/examples/middlewares-custom-decorators/context.ts new file mode 100644 index 000000000..c0783dd95 --- /dev/null +++ b/examples/middlewares-custom-decorators/context.ts @@ -0,0 +1,5 @@ +import User from "./user"; + +export interface Context { + currentUser: User; +} diff --git a/examples/middlewares-custom-decorators/decorators/current-user.ts b/examples/middlewares-custom-decorators/decorators/current-user.ts new file mode 100644 index 000000000..1549b96ea --- /dev/null +++ b/examples/middlewares-custom-decorators/decorators/current-user.ts @@ -0,0 +1,6 @@ +import { createParamDecorator } from "../../../src"; +import { Context } from "../context"; + +export default function CurrentUser() { + return createParamDecorator(({ context }) => context.currentUser); +} diff --git a/examples/middlewares/decorators/validate-args.ts b/examples/middlewares-custom-decorators/decorators/validate-args.ts similarity index 78% rename from examples/middlewares/decorators/validate-args.ts rename to examples/middlewares-custom-decorators/decorators/validate-args.ts index 252606992..da660fc5a 100644 --- a/examples/middlewares/decorators/validate-args.ts +++ b/examples/middlewares-custom-decorators/decorators/validate-args.ts @@ -1,11 +1,11 @@ import { plainToClass } from "class-transformer"; import { validate } from "class-validator"; -import { ClassType, ArgumentValidationError, UseMiddleware } from "../../../src"; +import { ClassType, ArgumentValidationError, createMethodDecorator } from "../../../src"; // sample implementation of custom validation decorator // this example use `class-validator` however you can plug-in `joi` or any other lib export function ValidateArgs(type: ClassType) { - return UseMiddleware(async ({ args }, next) => { + return createMethodDecorator(async ({ args }, next) => { const instance = plainToClass(type, args); const validationErrors = await validate(instance); if (validationErrors.length > 0) { diff --git a/examples/middlewares/examples.gql b/examples/middlewares-custom-decorators/examples.gql similarity index 100% rename from examples/middlewares/examples.gql rename to examples/middlewares-custom-decorators/examples.gql diff --git a/examples/middlewares/index.ts b/examples/middlewares-custom-decorators/index.ts similarity index 74% rename from examples/middlewares/index.ts rename to examples/middlewares-custom-decorators/index.ts index d158d2335..3f5f65f32 100644 --- a/examples/middlewares/index.ts +++ b/examples/middlewares-custom-decorators/index.ts @@ -6,6 +6,7 @@ import { buildSchema } from "../../src"; import { RecipeResolver } from "./recipe/recipe.resolver"; import { ResolveTimeMiddleware } from "./middlewares/resolve-time"; import { ErrorLoggerMiddleware } from "./middlewares/error-logger"; +import { Context } from "./context"; async function bootstrap() { // build TypeGraphQL executable schema @@ -16,7 +17,18 @@ async function bootstrap() { }); // Create GraphQL server - const server = new ApolloServer({ schema }); + const server = new ApolloServer({ + schema, + context: (): Context => { + return { + // example user + currentUser: { + id: 123, + name: "Sample user", + }, + }; + }, + }); // Start the server const { url } = await server.listen(4000); diff --git a/examples/middlewares/logger.ts b/examples/middlewares-custom-decorators/logger.ts similarity index 100% rename from examples/middlewares/logger.ts rename to examples/middlewares-custom-decorators/logger.ts diff --git a/examples/middlewares/middlewares/error-logger.ts b/examples/middlewares-custom-decorators/middlewares/error-logger.ts similarity index 89% rename from examples/middlewares/middlewares/error-logger.ts rename to examples/middlewares-custom-decorators/middlewares/error-logger.ts index 2fbe3a764..4b9ef0e2b 100644 --- a/examples/middlewares/middlewares/error-logger.ts +++ b/examples/middlewares-custom-decorators/middlewares/error-logger.ts @@ -2,7 +2,6 @@ import { Service } from "typedi"; import { MiddlewareInterface, NextFn, ResolverData, ArgumentValidationError } from "../../../src"; import { Context } from "../context"; -import { Middleware } from "../../../src/interfaces/Middleware"; import { Logger } from "../logger"; @Service() @@ -17,7 +16,7 @@ export class ErrorLoggerMiddleware implements MiddlewareInterface { message: err.message, operation: info.operation.operation, fieldName: info.fieldName, - userName: context.username, + userName: context.currentUser.name, }); if (!(err instanceof ArgumentValidationError)) { // hide errors from db like printing sql query diff --git a/examples/middlewares/middlewares/log-access.ts b/examples/middlewares-custom-decorators/middlewares/log-access.ts similarity index 72% rename from examples/middlewares/middlewares/log-access.ts rename to examples/middlewares-custom-decorators/middlewares/log-access.ts index 5fb38637f..7f6e4aed6 100644 --- a/examples/middlewares/middlewares/log-access.ts +++ b/examples/middlewares-custom-decorators/middlewares/log-access.ts @@ -9,8 +9,9 @@ export class LogAccessMiddleware 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}`); + this.logger.log( + `Logging access: ${context.currentUser.name} -> ${info.parentType.name}.${info.fieldName}`, + ); return next(); } } diff --git a/examples/middlewares/middlewares/number-interceptor.ts b/examples/middlewares-custom-decorators/middlewares/number-interceptor.ts similarity index 100% rename from examples/middlewares/middlewares/number-interceptor.ts rename to examples/middlewares-custom-decorators/middlewares/number-interceptor.ts diff --git a/examples/middlewares/middlewares/resolve-time.ts b/examples/middlewares-custom-decorators/middlewares/resolve-time.ts similarity index 100% rename from examples/middlewares/middlewares/resolve-time.ts rename to examples/middlewares-custom-decorators/middlewares/resolve-time.ts diff --git a/examples/middlewares/recipe/recipe.args.ts b/examples/middlewares-custom-decorators/recipe/recipe.args.ts similarity index 100% rename from examples/middlewares/recipe/recipe.args.ts rename to examples/middlewares-custom-decorators/recipe/recipe.args.ts diff --git a/examples/middlewares/recipe/recipe.resolver.ts b/examples/middlewares-custom-decorators/recipe/recipe.resolver.ts similarity index 72% rename from examples/middlewares/recipe/recipe.resolver.ts rename to examples/middlewares-custom-decorators/recipe/recipe.resolver.ts index 517343967..f11803ab0 100644 --- a/examples/middlewares/recipe/recipe.resolver.ts +++ b/examples/middlewares-custom-decorators/recipe/recipe.resolver.ts @@ -1,9 +1,11 @@ -import { Resolver, Query, Args, UseMiddleware } from "../../../src"; +import { Resolver, Query, Args } from "../../../src"; import recipeSamples from "./recipe.samples"; import { Recipe } from "./recipe.type"; import { RecipesArgs } from "./recipe.args"; import { ValidateArgs } from "../decorators/validate-args"; +import CurrentUser from "../decorators/current-user"; +import User from "../user"; @Resolver(of => Recipe) export class RecipeResolver { @@ -14,7 +16,9 @@ export class RecipeResolver { async recipes( @Args({ validate: false }) // disable built-in validation here options: RecipesArgs, + @CurrentUser() currentUser: User, ): Promise { + console.log(`User "${currentUser.name}" queried for recipes!`); const start = options.skip; const end = options.skip + options.take; return await this.items.slice(start, end); diff --git a/examples/middlewares/recipe/recipe.samples.ts b/examples/middlewares-custom-decorators/recipe/recipe.samples.ts similarity index 100% rename from examples/middlewares/recipe/recipe.samples.ts rename to examples/middlewares-custom-decorators/recipe/recipe.samples.ts diff --git a/examples/middlewares/recipe/recipe.type.ts b/examples/middlewares-custom-decorators/recipe/recipe.type.ts similarity index 100% rename from examples/middlewares/recipe/recipe.type.ts rename to examples/middlewares-custom-decorators/recipe/recipe.type.ts diff --git a/examples/middlewares-custom-decorators/user.ts b/examples/middlewares-custom-decorators/user.ts new file mode 100644 index 000000000..4852b6b00 --- /dev/null +++ b/examples/middlewares-custom-decorators/user.ts @@ -0,0 +1,4 @@ +export default interface User { + id: number; + name: string; +} diff --git a/examples/middlewares/context.ts b/examples/middlewares/context.ts deleted file mode 100644 index 5caef6bb5..000000000 --- a/examples/middlewares/context.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Context { - username?: string; -} diff --git a/src/decorators/createMethodDecorator.ts b/src/decorators/createMethodDecorator.ts new file mode 100644 index 000000000..a81b0583d --- /dev/null +++ b/src/decorators/createMethodDecorator.ts @@ -0,0 +1,8 @@ +import { UseMiddleware } from "./UseMiddleware"; +import { MiddlewareFn } from "../interfaces/Middleware"; + +export function createMethodDecorator( + resolver: MiddlewareFn, +): MethodDecorator { + return UseMiddleware(resolver); +} diff --git a/src/decorators/createParamDecorator.ts b/src/decorators/createParamDecorator.ts new file mode 100644 index 000000000..9f3a288b2 --- /dev/null +++ b/src/decorators/createParamDecorator.ts @@ -0,0 +1,20 @@ +import { ResolverData } from "../interfaces"; +import { getMetadataStorage } from "../metadata/getMetadataStorage"; +import { SymbolKeysNotSupportedError } from "../errors"; + +export function createParamDecorator( + resolver: (resolverData: ResolverData) => any, +): ParameterDecorator { + return (prototype, propertyKey, parameterIndex) => { + if (typeof propertyKey === "symbol") { + throw new SymbolKeysNotSupportedError(); + } + getMetadataStorage().collectHandlerParamMetadata({ + kind: "custom", + target: prototype.constructor, + methodName: propertyKey, + index: parameterIndex, + resolver, + }); + }; +} diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 139b09b86..11dbbd9db 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -2,6 +2,8 @@ export { Arg } from "./Arg"; export { Args } from "./Args"; export { ArgsType } from "./ArgsType"; export { Authorized } from "./Authorized"; +export { createParamDecorator } from "./createParamDecorator"; +export { createMethodDecorator } from "./createMethodDecorator"; export { Ctx } from "./Ctx"; export { registerEnumType } from "./enums"; export { Field } from "./Field"; diff --git a/src/metadata/definitions/param-metadata.ts b/src/metadata/definitions/param-metadata.ts index c2db61cbb..85bfcdfb9 100644 --- a/src/metadata/definitions/param-metadata.ts +++ b/src/metadata/definitions/param-metadata.ts @@ -1,6 +1,7 @@ import { ValidatorOptions } from "class-validator"; import { TypeValueThunk, TypeOptions } from "../../decorators/types"; +import { ResolverData } from "../../interfaces"; export interface BasicParamMetadata { target: Function; @@ -36,6 +37,10 @@ export interface ArgParamMetadata extends CommonArgMetadata { export interface ArgsParamMetadata extends CommonArgMetadata { kind: "args"; } +export interface CustomParamMetadata extends BasicParamMetadata { + kind: "custom"; + resolver: (resolverData: ResolverData) => any; +} // prettier-ignore export type ParamMetadata = | InfoParamMetadata @@ -44,4 +49,5 @@ export type ParamMetadata = | RootParamMetadata | ArgParamMetadata | ArgsParamMetadata + | CustomParamMetadata ; diff --git a/src/resolvers/helpers.ts b/src/resolvers/helpers.ts index 7c4fdee65..c21eb9728 100644 --- a/src/resolvers/helpers.ts +++ b/src/resolvers/helpers.ts @@ -11,7 +11,7 @@ import { AuthMiddleware } from "../helpers/auth-middleware"; export async function getParams( params: ParamMetadata[], - { root, args, context, info }: ResolverData, + resolverData: ResolverData, globalValidate: boolean | ValidatorOptions, pubSub: PubSubEngine, ): Promise { @@ -22,34 +22,38 @@ export async function getParams( switch (paramInfo.kind) { case "args": return await validateArg( - convertToType(paramInfo.getType(), args), + convertToType(paramInfo.getType(), resolverData.args), globalValidate, paramInfo.validate, ); case "arg": return await validateArg( - convertToType(paramInfo.getType(), args[paramInfo.name]), + convertToType(paramInfo.getType(), resolverData.args[paramInfo.name]), globalValidate, paramInfo.validate, ); case "context": if (paramInfo.propertyName) { - return context[paramInfo.propertyName]; + return resolverData.context[paramInfo.propertyName]; } - return context; + return resolverData.context; case "root": - const rootValue = paramInfo.propertyName ? root[paramInfo.propertyName] : root; + const rootValue = paramInfo.propertyName + ? resolverData.root[paramInfo.propertyName] + : resolverData.root; if (!paramInfo.getType) { return rootValue; } return convertToType(paramInfo.getType(), rootValue); case "info": - return info; + return resolverData.info; case "pubSub": if (paramInfo.triggerKey) { return (payload: any) => pubSub.publish(paramInfo.triggerKey!, payload); } return pubSub; + case "custom": + return await paramInfo.resolver(resolverData); } }), ); diff --git a/tests/functional/middlewares.ts b/tests/functional/middlewares.ts index 8f17d7818..cc170d05b 100644 --- a/tests/functional/middlewares.ts +++ b/tests/functional/middlewares.ts @@ -20,6 +20,7 @@ import { NextFn, ResolverData, } from "../../src"; +import { createMethodDecorator } from "../../src/decorators/createMethodDecorator"; describe("Middlewares", () => { let schema: GraphQLSchema; @@ -98,6 +99,10 @@ describe("Middlewares", () => { return result; } } + const CustomMethodDecorator = createMethodDecorator(async (resolverData, next) => { + middlewareLogs.push("CustomMethodDecorator"); + return next(); + }); @ObjectType() class SampleObject { @@ -200,6 +205,13 @@ describe("Middlewares", () => { return "classMiddlewareQueryResult"; } + @Query() + @CustomMethodDecorator + customMethodDecoratorQuery(): string { + middlewareLogs.push("customMethodDecoratorQuery"); + return "customMethodDecoratorQuery"; + } + @FieldResolver() @UseMiddleware(fieldResolverMiddleware) resolverField(): string { @@ -367,7 +379,7 @@ describe("Middlewares", () => { classMiddlewareQuery }`; - const { data, errors } = await graphql(schema, query); + const { data } = await graphql(schema, query); expect(data!.classMiddlewareQuery).toEqual("classMiddlewareQueryResult"); expect(middlewareLogs).toHaveLength(3); @@ -376,6 +388,19 @@ describe("Middlewares", () => { expect(middlewareLogs[2]).toEqual("ClassMiddleware after"); }); + it("should correctly call resolver of custom method decorator", async () => { + const query = `query { + customMethodDecoratorQuery + }`; + + const { data } = await graphql(schema, query); + + expect(data!.customMethodDecoratorQuery).toEqual("customMethodDecoratorQuery"); + expect(middlewareLogs).toHaveLength(2); + expect(middlewareLogs[0]).toEqual("CustomMethodDecorator"); + expect(middlewareLogs[1]).toEqual("customMethodDecoratorQuery"); + }); + it("should call middlewares for normal field", async () => { const query = `query { sampleObjectQuery { diff --git a/tests/functional/resolvers.ts b/tests/functional/resolvers.ts index 4758e398d..8bc270b2a 100644 --- a/tests/functional/resolvers.ts +++ b/tests/functional/resolvers.ts @@ -47,6 +47,7 @@ import { ConflictingDefaultValuesError, ConflictingDefaultWithNullableError, WrongNullableListOptionError, + createParamDecorator, } from "../../src"; import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; import { getSchemaInfo } from "../helpers/getSchemaInfo"; @@ -1082,6 +1083,8 @@ describe("Resolvers", () => { let queryRoot: any; let queryContext: any; let queryInfo: any; + let queryFirstCustom: any; + let querySecondCustom: any; let descriptorEvaluated: boolean; let sampleObjectConstructorCallCount: number; @@ -1119,6 +1122,8 @@ describe("Resolvers", () => { queryRoot = undefined; queryContext = undefined; queryInfo = undefined; + queryFirstCustom = undefined; + querySecondCustom = undefined; descriptorEvaluated = false; sampleObjectConstructorCallCount = 0; }); @@ -1126,6 +1131,9 @@ describe("Resolvers", () => { beforeAll(async () => { getMetadataStorage().clear(); + const FirstCustomArgDecorator = () => createParamDecorator(resolverData => resolverData); + const SecondCustomArgDecorator = (arg: string) => createParamDecorator(async () => arg); + @ArgsType() class SampleArgs { private readonly TRUE = true; @@ -1235,6 +1243,16 @@ describe("Resolvers", () => { return true; } + @Query() + queryWithCustomDecorators( + @FirstCustomArgDecorator() firstCustom: any, + @SecondCustomArgDecorator("secondCustom") secondCustom: any, + ): boolean { + queryFirstCustom = firstCustom; + querySecondCustom = secondCustom; + return true; + } + @Query() @DescriptorDecorator() queryWithCustomDescriptorDecorator(): boolean { @@ -1551,6 +1569,23 @@ describe("Resolvers", () => { expect(queryContext).toEqual("present"); }); + it("should inject resolver data to custom arg decorator resolver and return its value", async () => { + const query = /* graphql */ ` + query { + queryWithCustomDecorators + } + `; + const root = { rootField: 2 }; + const context = { contextField: "present" }; + + await graphql(schema, query, root, context); + + expect(queryFirstCustom.root).toEqual(root); + expect(queryFirstCustom.context).toEqual(context); + expect(queryFirstCustom.info).toBeDefined(); + expect(querySecondCustom).toEqual("secondCustom"); + }); + it("should allow for overwriting descriptor value in custom decorator", async () => { const query = /* graphql */ ` query { diff --git a/website/i18n/en.json b/website/i18n/en.json index 4875c853f..43186e8fc 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -17,6 +17,9 @@ "complexity": { "title": "Query complexity" }, + "custom-decorators": { + "title": "Custom decorators" + }, "dependency-injection": { "title": "Dependency injection" }, diff --git a/website/sidebars.json b/website/sidebars.json index 72d9596fd..a69baf93e 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -16,6 +16,7 @@ "inheritance", "generic-types", "middlewares", + "custom-decorators", "complexity" ], "Others": ["emit-schema", "browser-usage"]