type-graphql
type-graphql copied to clipboard
Update directives docs
Since graphql-utils had updated their package to version 8.0.0, the current docs don't help users to build their resolvers with directives into the new format of graphql-utils. It'll be awesome if you guys can insert some examples of how to upgrade to version 8.0.0. Since SchemaDirectiveVisitor.visitSchemaDirectives has been removed and with makeExecutableSchema don't include the @Directive decorator into SDL, I don't know how to make the @Directive appear into resolver SDL.
@leorrodrigues did you ever figure this out?
@jerrywithaz yeap, it took me a while to discover how to upgrade the version and keep the directive working fine. In principle, you have to change your directive code to be a pure function, that receives a schema and returns the mapSchema from @graphql-tools/utils. The mapSchema receives two options: the schema and one object with the mapperKind (the type of value the directive is, like ROOT_FIELD, FIELD, QUERY, MUTATION, and so on) as its key, while for values you have to send another function, which finally is the code of your directive. Still, to build the schema, I'm using the buildSchemaSync function and after that, I pass this schema to the directive function.
Here is a sample code that can help you, I let two directives in the example, for OBJECT_FIELD and ROOT_FIELD. To build the Schema:
const schema = buildSchemaSync({ resolvers });
//schemaDirectives is an array with all directives that you want to be applied in the schema
const schemaWithDirectives = schemaDirectives.reduce(
(newSchema, directive) => directive(newSchema),
schema,
);
In the ROOT_FIELD directive, you can apply directives in the QUERY and MUTATION directly:
@Query(() => [ResponseData])
@Directive('@directiveExample1(requires: [requiredArgument])')
async queryName() {
... extra code
}
While the OBJECT_FIELD directive, you achieve:
@ObjectType()
export default class SomeClass {
@Field()
@Directive('@directiveExample2(requires: [requiredArgument])')
someField: someType;
}
The directive code structure for the first example is:
const exampleDirective1 = (schema: GraphQLSchema) =>
mapSchema(schema, {
[MapperKind.ROOT_FIELD]: fieldConfig => {
if (!fieldConfig.astNode) return fieldConfig;
const { directives } = fieldConfig.astNode;
if (!directives) return fieldConfig;
const directive = getDirective(directives as any, 'hasRole');
if (!directive) return fieldConfig;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async (...args) => {
your directive code...
};
return fieldConfig;
},
});
For the second example, you have:
const exampleDirective2 = (schema: GraphQLSchema) =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: fieldConfig => {
if (!fieldConfig.astNode) return fieldConfig;
const { directives } = fieldConfig.astNode;
if (!directives) return fieldConfig;
const directive = getDirective(directives as any, 'hasRole');
if (!directive) return fieldConfig;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async (...args) => {
your directive code...
};
return fieldConfig;
},
});
But if you see, we need a function called getDirective, provided by @graphql-tools, but for some reason, it always returns undefined so I've implemented my own function to get that.
import { DirectiveNode } from 'graphql';
const getDirective = (directives: DirectiveNode[], name: string) =>
directives.find(item => item.name.value === name);
With this sample, you can customize and implement it in your needs, I hope it helps you.
Ah, that is awesome man, I was on the right track but this helped me figure it out, nice work!
I ended up creating a custom function like this:
import { GraphQLSchema } from "graphql";
import { buildSchema, BuildSchemaOptions } from "type-graphql";
import { TypeSource } from "@graphql-tools/utils";
import { mergeSchemas } from "@graphql-tools/schema";
type SchemaDirectives = {
typeDef: TypeSource;
directive: (schema: GraphQLSchema) => GraphQLSchema;
}
/** Enables a schema to be build with schema directives and optional additional typeDefs. */
async function buildSchemaWithDirectives({
schemaDirectives,
typeDefs,
...options
}: BuildSchemaOptions & { schemaDirectives: SchemaDirectives[], typeDefs?: TypeSource[] }): Promise<GraphQLSchema> {
const schema = await buildSchema(options);
// Add type defs for schema directives to the schema with directives
// The type defs must be added first before executing the schema mappers
// Or else `getDirectives` from `@graphql-tools/utils` will return undefined
const schemaMerged = mergeSchemas({
schemas: [schema],
typeDefs: schemaDirectives.map((schemaDirective) => schemaDirective.typeDef),
});
// Executes the schema mapper for each directive and applies it to the generated schema
const schemaWithDirectives = schemaDirectives.reduce(
(newSchema, { directive }) => directive(newSchema),
schemaMerged,
);
return schemaWithDirectives;
}
export default buildSchemaWithDirectives;
And then my getDirective function also appends the directive to the field definition description as such:
import { GraphQLFieldConfig } from "graphql";
/** Finds a directive from the astNode of a field config. Also updates the description of a field with the directive. */
const getDirective = (
fieldConfig: GraphQLFieldConfig<any, any>,
name: string,
typeDef: string
) => {
const astNode = fieldConfig.astNode;
const directives = astNode?.directives || [];
const directive = directives.find((item) => item.name.value === name);
if (directive) {
fieldConfig.description = `${fieldConfig.description || ""} \n${typeDef}`;
}
return directives.find((item) => item.name.value === name);
};
export default getDirective;
@leorrodrigues @jerrywithaz .. Wonderful stuff guys. I believe this must be working well for you guys. While I was looking at both the solutions, correct me If I'm wrong . The only difference I can spot between the first and second solution is the introduction of typeDefs in the second one. With the above implementation, is it possible to print a SDL with directives ?
I'm quite confused what shall I put up in typeDefs ? Do I put only those fields in which directives are required in the form of string ?
Can you please give an example, how have you guys consumed this implementation in your respective use cases ?
@leorrodrigues , @jerrywithaz , thanks for sharing the workaround. Just thinking aloud here using workaround is good idea? I think using getDirective from graphql-tools make sense to me.
I know, its not working at this moment with type-graphql. IMO, if type-graphql adds the directive definition to typedefs, getDirective should work out of the box.
e.g.
directive @upper on FIELD_DEFINITION
I tried below things as a workaround, but it does not work out
// resolvers are defined with type-graphql
let { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ resolvers });
// add directive definition
typeDefs.concat(`
directive @upper on FIELD_DEFINITION
`);
// Create the base executable schema
let schema = makeExecutableSchema({
typeDefs,
resolvers
});
// Transform the schema by applying directive logic
// upperDirectiveTransformer is utility method using graphql-tools
// https://www.graphql-tools.com/docs/schema-directives
schema = upperDirectiveTransformer(schema, 'upper');
// use schema with apollo server
const server = new ApolloServer({schema});
But, looks like buildTypeDefsAndResolvers does not populate @Directives in astNode.
@MichalLytek , thoughts?
@RishikeshDarandale It should be stated in docs that buildTypeDefsAndResolvers is using printSchema which does not support directives. There's only a warning about query complexity.
Thanks @MichalLytek .
What would be best way to include directive definition in schema?
So, OOB getDirective from graphql-tools works as expected.
@jerrywithaz awesome work! In case I want to use directives to validate a user has authorization to access to a query/mutation base on his role, should I implement this logic in the context param of the graphql server?
Thanks @MichalLytek . What would be best way to include directive definition in schema? So, OOB
getDirectivefromgraphql-toolsworks as expected.
@MichalLytek , Is there any way to add directive @upper on FIELD_DEFINITION to schema with @Directive support?
@RishikeshDarandale
export const upperCaseDirective = new GraphQLDirective({
name: "upper",
locations: [DirectiveLocation.FIELD_DEFINITION],
});
I stumbled across this issue when trying to get directives to work using type-graphql and an Apollo federated server. I was able to get it working using printSchemaWithDirectives
const schema = await buildSchema({
resolvers: [PersonResolver]
});
const typeDefs = gql(printSchemaWithDirectives(schema));
const resolvers = createResolversMap(schema);
const federatedSchema = buildFederatedSchema({
typeDefs,
resolvers,
});
SchemaDirectiveVisitor.visitSchemaDirectives(federatedSchema, directives);
Hey, I want to share my authDirective based on graphql-tools Schema Directives and TypeGraphQL Authorization.
I hope you find this useful (please share your thoughts and suggestions or criticisms):
authDirective.ts:
import {
GraphQLSchema,
defaultFieldResolver,
GraphQLFieldConfig
} from 'graphql';
import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils';
import { container } from 'tsyringe'; // Change with your IOC container
import { ClassType, ResolverData } from 'type-graphql';
import { AuthenticationError, AuthorizationError } from '~/errors'; // Auth* errors
export type AuthData<TRoles> = {
roles: TRoles[];
};
export type AuthFn<TRoles, TContext = Record<string, unknown>> = (
resolverData: ResolverData<TContext>,
authData: AuthData<TRoles>
) => boolean | Promise<boolean>;
export type AuthFnClass<TRoles, TContext = Record<string, unknown>> = {
auth(
resolverData: ResolverData<TContext>,
authData: AuthData<TRoles>
): boolean | Promise<boolean>;
};
export type Auth<TRoles, TContext = Record<string, unknown>> =
| AuthFn<TRoles, TContext>
| ClassType<AuthFnClass<TRoles, TContext>>;
export enum AuthMode {
ERROR = 'ERROR',
NULL = 'NULL'
}
export type AuthDirective<TRoles, TContext = Record<string, unknown>> = {
directiveName: string;
rolesName: string;
auth: Auth<TRoles, TContext>;
authMode?: AuthMode;
};
type AuthDirectiveLogic<
TRoles,
TContext = Record<string, unknown>
> = AuthDirective<TRoles, TContext> & {
typeDirectiveArgumentMaps: Record<string, unknown>;
schema: GraphQLSchema;
fieldConfig: GraphQLFieldConfig<unknown, TContext>;
typeName: string;
};
function authDirectiveLogic<TRoles, TContext = Record<string, unknown>>({
schema,
fieldConfig,
directiveName,
typeDirectiveArgumentMaps,
typeName,
auth,
authMode
}: AuthDirectiveLogic<TRoles, TContext>) {
const authGraphQLDirective = (getDirective(
schema,
fieldConfig,
directiveName
)?.[0] ?? typeDirectiveArgumentMaps[typeName]) as
| undefined
| null
| { roles?: TRoles[] };
if (
!authGraphQLDirective || // 'null' or 'undefined'
!authGraphQLDirective.roles || // 'roles' is 'null' or 'undefined'
!Array.isArray(authGraphQLDirective.roles) // No 'roles' array
) {
return fieldConfig;
}
const { roles } = authGraphQLDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
// eslint-disable-next-line no-param-reassign
fieldConfig.resolve = async (root, args, context, info) => {
let accessGranted: boolean;
const resolverData: ResolverData<TContext> = {
root,
args,
context,
info
};
const authData: AuthData<TRoles> = { roles };
if (auth.prototype) {
// Auth class
const authInstance = container.resolve(
auth as ClassType<AuthFnClass<TRoles, TContext>>
);
accessGranted = await authInstance.auth(resolverData, authData);
} else {
// Auth function
accessGranted = await (auth as AuthFn<TRoles, TContext>)(
resolverData,
authData
);
}
if (!accessGranted) {
switch (authMode) {
case AuthMode.NULL:
return null;
case AuthMode.ERROR:
default:
throw roles.length === 0
? new AuthenticationError()
: new AuthorizationError();
}
}
return resolve(root, args, context, info);
};
return fieldConfig;
}
export function authDirective<TRoles, TContext = Record<string, unknown>>(
args: AuthDirective<TRoles, TContext>
) {
const typeDirectiveArgumentMaps: Record<string, unknown> = {};
return {
typeDef: `
directive @${args.directiveName}(
roles: [${args.rolesName}!]! = [],
) on OBJECT | FIELD_DEFINITION`,
transformer: (schema: GraphQLSchema) => {
return mapSchema(schema, {
[MapperKind.TYPE]: (type) => {
const authGraphQLDirective = getDirective(
schema,
type,
args.directiveName
)?.[0];
if (authGraphQLDirective) {
typeDirectiveArgumentMaps[type.name] = authGraphQLDirective;
}
return undefined;
},
[MapperKind.ROOT_FIELD]: (fieldConfig, _, typeName) =>
authDirectiveLogic({
...args,
typeDirectiveArgumentMaps,
schema,
fieldConfig,
typeName
}),
[MapperKind.OBJECT_FIELD]: (fieldConfig, _, typeName) =>
authDirectiveLogic({
...args,
typeDirectiveArgumentMaps,
schema,
fieldConfig,
typeName
})
});
}
};
}
Then you can build your auth directive (e.g. for a user) as follows:
authUserDirective.ts
import { authDirective, AuthFn } from './authDirective';
// Remember to register enum in schema
export enum UserRoles {
ADMIN = 'ADMIN',
USER = 'USER'
}
export type Context = {
user?: { id: string; roles: UserRoles[] };
};
const authFn: AuthFn<UserRoles, Context> = (
{ context: { user } },
{ roles }
) => {
if (!user) {
// No user
return false;
}
if (roles.length === 0) {
// '@auth' without roles
return true;
}
// Check user roles overlap
return user.roles.some((role) => roles.includes(role));
};
export const authUserDirective = authDirective({
directiveName: 'auth',
rolesName: 'UserRoles',
auth: authFn
});
Use it in ObjectType, Field, Query or Mutation:
@Query(() => [ResponseData])
@Directive(`@auth(roles: [ADMIN])`)
async queryName() { // ... }
@ObjectType()
@Directive(`@auth`)
export default class SomeClass {
@Field()
@Directive(`@auth(roles: [ADMIN])`)
privateToAdmin: someType;
@Field()
publicForAllAuth: string;
}
And build the schema (thanks @jerrywithaz) as follows:
schema.ts:
import { GraphQLSchema } from 'graphql';
import { buildSchemaSync } from 'type-graphql';
import { mergeSchemas } from '@graphql-tools/schema';
import { container } from 'tsyringe'; // Change with your IOC container
import { authUserDirective } from './directives';
import { UserResolver } from './resolvers';
// Directives
const directives = [authUserDirective] as const;
// Resolvers
const resolvers = [UserResolver] as const;
const schemaSimple = buildSchemaSync({
resolvers,
container: { get: (cls) => container.resolve(cls) }
});
const schemaMerged = mergeSchemas({
schemas: [schemaSimple],
typeDefs: directives.map((directive) => directive.typeDef)
});
export const schema: GraphQLSchema = directives.reduce(
(newSchema, { transformer }) => transformer(newSchema),
schemaMerged
);
Hey everyone, if you are struggling with @auth directive I've created the graphql-auth-directive library.
It's fully configurable and compatible with vanilla GraphQL and TypeGraphQL. See this.
I hope it will be useful :wave:
I think implementing directives is out of TypeGraphQL scope for now.
The updated codebase now uses this approach: https://github.com/MichalLytek/type-graphql/blob/master/tests/helpers/directives/TestDirective.ts
I'm not sure if the issue is strictly about the snippet in the docs: https://typegraphql.com/docs/directives.html#providing-the-implementation