Additional decorators for CRUD resolvers and Prisma classes and fields
Additional decorators for Prisma schema resolvers
When you need to apply some decorators like @Authorized
, @UseMiddleware
or @Extensions
on the generated resolvers methods, you don't need to modify the generated source files.
To support this, typegraphql-prisma
generates two things: applyResolversEnhanceMap
function and a ResolversEnhanceMap
type. All you need to do is to create a config object, where you put the decorator functions (without @
) in an array, and then call that function with that config. Remember that it has to be done before building the schema, eg.:
import {
resolvers,
ResolversEnhanceMap,
applyResolversEnhanceMap,
} from "@generated/type-graphql";
import { Authorized } from "type-graphql";
const resolversEnhanceMap: ResolversEnhanceMap = {
Category: {
createCategory: [Authorized(Role.ADMIN)],
},
};
applyResolversEnhanceMap(resolversEnhanceMap);
const schema = await buildSchema({
resolvers,
validate: false,
});
This way, when you call createCategory
GraphQL mutation, it will trigger the type-graphql
authChecker
function, providing a Role.ADMIN
role, just like you would put the @Authorized
on top of the resolver method.
Also, if you have a large schema and you need to provide plenty of decorators, you can split the config definition into multiple smaller objects placed in different files.
To accomplish this, just import the generic ResolverActionsConfig
type and define the resolvers config separately for every Prisma schema model, e.g:
import {
ResolversEnhanceMap,
ResolverActionsConfig,
applyResolversEnhanceMap,
} from "@generated/type-graphql";
import { Authorized, Extensions } from "type-graphql";
// define the decorators config using generic ResolverActionsConfig<TModelName> type
const categoryActionsConfig: ResolverActionsConfig<"Category"> = {
deleteCategory: [
Authorized(Role.ADMIN),
Extensions({ logMessage: "Danger zone", logLevel: LogLevel.WARN }),
],
};
const problemActionsConfig: ResolverActionsConfig<"Problem"> = {
createProblem: [Authorized()],
};
// join the actions config into a single resolvers enhance object
const resolversEnhanceMap: ResolversEnhanceMap = {
Category: categoryActionsConfig,
Problem: problemActionsConfig,
};
// apply the config (it will apply decorators on the resolver class methods)
applyResolversEnhanceMap(resolversEnhanceMap);
If you want to apply some decorators on all the methods of a model CRUD resolver, you can use the special _all
property to achieve that:
applyResolversEnhanceMap({
Client: {
_all: [Authorized()],
},
});
However, be aware that this will apply the decorators on all the methods of the resolver. The decorators will be combined together with the ones provided for selected methods:
applyResolversEnhanceMap({
Client: {
_all: [Authorized()],
deleteClient: [Extensions({ logMessage: "Danger zone" })],
},
});
In some cases, this might not be the desired behavior, e.g. when you define the @Authorized
decorator rules on the _all
property, but then you want override that and provide different roles for some query or make it a public one.
To accomplish this, you need to use the function variant of the ResolverActionsConfig
, which is supposed to return a new array of decorators that will be applied on the selected method:
applyResolversEnhanceMap({
Story: {
_all: [Authorized(Role.ADMIN, Role.MEMBER)],
createStory: () => [Authorized(Role.SUPER_ADMIN)], // require higher role
story: () => [], // make it public
},
});
It also takes the _all
decorators as a parameter, so you can leverage that to combine the _all
and selected method decorators in a desired way:
applyResolversEnhanceMap({
Client: {
_all: [Extensions({ logMessage: "Fun zone" }), Authorized()],
deleteClient:
// ignore log message extension
([_logExtension, auth]) => [
// provide own message
Extensions({ logMessage: "Danger zone" }),
auth,
],
},
});
You can also use the _query
and _mutation
shorthands to apply decorators only to the queries or mutations, e.g.:
applyResolversEnhanceMap({
Client: {
_all: [UseMiddleware(LogMiddleware)],
_query: [Authorized()],
_mutation: [Authorized(Role.ADMIN)], // only admin can change the data
},
});
Additional decorators for Prisma schema classes and fields
If you need to apply some decorators, like @Authorized
or @Extensions
, on the model @ObjectType
and its fields, you can use similar pattern as for the resolver actions described above.
All you need to do is to import ModelsEnhanceMap
type and applyModelsEnhanceMap
function, and then create a config object, where you put the decorator functions (without @
) in an array.
If you want to split the config definitions, you can use ModelConfig
type in the same way like ResolverActionsConfig
, e.g.:
import {
ModelsEnhanceMap,
ModelConfig,
applyModelsEnhanceMap,
} from "@generated/type-graphql";
import { Authorized, Extensions } from "type-graphql";
// define the decorators configs
const userEnhanceConfig: ModelConfig<"User"> = {
fields: {
email: [
Authorized(Role.ADMIN),
Extensions({ logMessage: "Danger zone", logLevel: LogLevel.WARN }),
],
},
};
const modelsEnhanceMap: ModelsEnhanceMap = {
User: userEnhanceConfig,
// or apply it inline
Recipe: {
class: [
// decorators for @ObjectType model class
Extensions({ logMessage: "Secret recipe", logLevel: LogLevel.INFO }),
],
fields: {
// decorator for model class fields
averageRating: [Authorized()],
},
},
};
// apply the config (it will apply decorators on the model class and its properties)
applyModelsEnhanceMap(modelsEnhanceMap);
This way, you can apply some rules on single model or its fields, like User.email
visible only for Admin.
If you want to apply some decorators on all the fields of a model, you can use the special _all
property to achieve that (which also can be overwritten using the function variant, like in the resolver actions config):
applyModelsEnhanceMap({
User: {
fields: {
// all fields are protected against unauthorized access
_all: [Authorized()],
// this field has additional decorators to apply
password: [
Extensions({ logMessage: "Danger zone", logLevel: LogLevel.WARN }),
],
// this field is public, no `@Authorized` decorator returned
id: allDecorators => [],
},
},
});
If you want to apply decorator to model's relation field, you need to use the applyRelationResolversEnhanceMap
function and RelationResolverActionsConfig<TModel>
type if you need to separate the configs. In order to apply some decorators on all the fields of a model, you can use the special _all
property to achieve that, and override that for some specific fields using the function variant:
const clientRelationEnhanceConfig: RelationResolverActionsConfig<"Client"> = {
posts: [
UseMiddleware((_data, next) => {
console.log("Client.posts relation field accessed");
return next();
}),
],
};
applyRelationResolversEnhanceMap({
Client: clientRelationEnhanceConfig,
Product: {
fields: {
// all relation fields are protected against unauthorized access
_all: [Authorized()],
// but designer relation field is public
designer: allDecorators => [],
},
},
});
In case of other output types like AggregateFooBar
, you can use the same pattern but this time using the applyOutputTypesEnhanceMap
function and OutputTypeConfig
or OutputTypesEnhanceMap
types:
const aggregateClientConfig: OutputTypeConfig<"AggregateClient"> = {
fields: {
avg: [Extensions({ complexity: 10 })],
},
};
applyOutputTypesEnhanceMap({
// separate config
AggregateClient: aggregateClientConfig,
// or an inline one
ClientAvgAggregate: {
fields: {
_all: [Extensions({ complexity: 10 })],
age: [Authorized()],
},
},
});
If you want to add decorators for input types or args classes, you can leverage applyArgsTypesEnhanceMap
and applyInputTypesEnhanceMap
functions and use ArgsTypesEnhanceMap
, ArgConfig<TArgsType>
, InputTypesEnhanceMap
, InputTypeConfig<TInput>
types if you want to split the definitions. The special _all
property also can apply the decorators to all the fields, with the ability to override it using the the function variant for other fields, like in the resolver actions config:
applyArgsTypesEnhanceMap({
CreateProblemArgs: {
fields: {
data: [ValidateNested()],
},
},
});
applyInputTypesEnhanceMap({
ProblemCreateInput: {
fields: {
_all: [Allow()],
problemText: allDecorators => [MinLength(10)],
},
},
});
Be aware that in case of class-validator
you need to add @ValidateNested
decorator to the args classes to trigger validation of the proper input types.