-
-
Notifications
You must be signed in to change notification settings - Fork 677
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4414f46
commit 23409fd
Showing
16 changed files
with
2,165 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
--- | ||
title: Query complexity | ||
id: version-1.1.1-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 | ||
|
||
```typescript | ||
@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: | ||
|
||
```typescript | ||
async function bootstrap() { | ||
// ...build TypeGraphQL schema as always | ||
|
||
// Create GraphQL server | ||
const server = new ApolloServer({ | ||
schema, | ||
// Create a plugin that will allow for query complexity calculation for every request | ||
plugins: [ | ||
{ | ||
requestDidStart: () => ({ | ||
didResolveOperation({ request, document }) { | ||
/** | ||
* This provides GraphQL query analysis to be able to react on complex queries to your GraphQL server. | ||
* This can be used to protect your GraphQL servers against resource exhaustion and DoS attacks. | ||
* More documentation can be found at https://github.com/ivome/graphql-query-complexity. | ||
*/ | ||
const complexity = getComplexity({ | ||
// Our built schema | ||
schema, | ||
// To calculate query complexity properly, | ||
// we have to check only the requested operation | ||
// not the whole document that may contains multiple operations | ||
operationName: request.operationName, | ||
// The GraphQL query document | ||
query: document, | ||
// The variables for our GraphQL query | ||
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 }), | ||
], | ||
}); | ||
// Here we can react to the calculated complexity, | ||
// like compare it with max and throw error when the threshold is reached. | ||
if (complexity > 20) { | ||
throw new Error( | ||
`Sorry, too complicated query! ${complexity} is over 20 that is the max allowed complexity.`, | ||
); | ||
} | ||
// And here we can e.g. subtract the complexity point from hourly API calls limit. | ||
console.log("Used query complexity points:", complexity); | ||
}, | ||
}), | ||
}, | ||
], | ||
}); | ||
|
||
// ...start the server as always | ||
} | ||
``` | ||
|
||
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/v1.1.1/examples/query-complexity). |
106 changes: 106 additions & 0 deletions
106
website/versioned_docs/version-1.1.1/custom-decorators.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
--- | ||
title: Custom decorators | ||
id: version-1.1.1-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<MyContextType>(({ 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<Recipe>) {} | ||
|
||
@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/MichalLytek/type-graphql/tree/v1.1.1/examples/middlewares-custom-decorators). |
Oops, something went wrong.