我有一个 NestJS GraphQL API,它有一个
User
对象,我可以这样查询
@ObjectType()
export class User {
@Field(() => ID)
id: string
@Field()
name: string
@Field()
password: string
}
如果提出请求的人获得授权,我希望他们能够检索整个对象,如果没有,则应排除
password
字段。理想情况下,我想在这个对象上使用某种属性。
我有自己的授权系统,用于细粒度的权限,但是为此目的并不重要。我尝试将
password
字段添加为单独的 fieldResolver,并带有基本防护来检查用户是否已登录:
@UseGuards(UserGuard)
@ResolveField(() => String)
async password(@Parent() user: User): Promise<string> {
return 'this is the password'
}
如果他们登录,它会按预期工作,我得到以下响应:
{
"id": "1",
"name": "John Doe",
"password": "this is the password"
}
但是,如果他们没有登录,我会收到
Unauthorized
错误并且数据为空:
{
errors: [
{
"message": "Unauthorized",
// more error stuff
}
],
data: null
}
我期望的是
password
字段未经授权,但其他字段的数据仍然返回,例如
{
errors: [
{
"message": "Unauthorized",
"path": ["password"]
}
],
data: {
"id": "1",
"name": "John Doe"
}
}
我知道这是 .NET GraphQL API 中的行为,所以想知道如何在 NestJS 中实现它?或者这不是我们想要的行为?
我创建了这个公共指令和字段装饰器
公共指令.ts
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { ForbiddenException } from '@nestjs/common';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';
export function publicDirectiveTransformer(schema: GraphQLSchema, directiveName: string) {
const typeDirectiveArgumentMaps: Record<string, any> = {};
return mapSchema(schema, {
[MapperKind.OBJECT_TYPE]: (objectType) => {
const publicDirective = getDirective(schema, objectType, directiveName)?.[0];
if (publicDirective) {
typeDirectiveArgumentMaps[objectType.name] = publicDirective;
}
return undefined;
},
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
const publicDirective =
getDirective(schema, fieldConfig, directiveName)?.[0] ??
typeDirectiveArgumentMaps[typeName];
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = (source, args, context, info) => {
if (context.user) return resolve(source, args, context, info);
const { path } = info;
// return to auth.guard for root level field query, mutation, subscription
if (!path.prev) return resolve(source, args, context, info);
// check fields for PublicProtected requests without token
if (!publicDirective)
throw new ForbiddenException({ message: `Can't access ${path.key}`, ctx: context });
return resolve(source, args, context, info);
};
return fieldConfig;
},
});
}
public.decorator.ts
import { applyDecorators, SetMetadata } from '@nestjs/common';
import {
Field,
FieldOptions,
ReturnTypeFunc,
ResolveField,
Directive,
ObjectType,
InterfaceType,
InterfaceTypeOptions,
ObjectTypeOptions,
} from '@nestjs/graphql';
export const PublicField = (returnTypeFunction?: ReturnTypeFunc, options?: FieldOptions) =>
applyDecorators(Directive('@public'), Field(returnTypeFunction, options));
export const PublicResolveField = (
name?: string,
returnTypeFunction?: ReturnTypeFunc,
options?: FieldOptions,
) => applyDecorators(Directive('@public'), ResolveField(name, returnTypeFunction, options));
export const PublicObjectType = (options?: ObjectTypeOptions) =>
applyDecorators(Directive('@public'), ObjectType(options ?? {}));
export const PublicInterfaceType = (options?: InterfaceTypeOptions) =>
applyDecorators(Directive('@public'), InterfaceType(options));
包含这个app.module.ts
GraphQLModule.forRoot<ApolloDriverConfig>({
...Config options
transformSchema: (schema) => publicDirectiveTransformer(schema, 'public'),
buildSchemaOptions: {
directives: [
new GraphQLDirective({
name: 'public',
locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT],
}),
],
},
});
现在你可以使用like
@ObjectType()
export class User {
@PublicField(() => ID)
id: string
@PublicField()
name: string
@Field()
password: string
}
或者您也可以使用
@PublicObjectType
或 @PublicInterfaceType
将整个班级设置为公共
注意: 如果授权完成,则需要在 GraphQL 上下文中设置
user
键,其检查用于 publicDirectiveTransformer