graphQL 自定义指令(Directives)

原文地址

GraphQL 内置指令

GraphQL 中内置了两款逻辑指令,指令跟在字段名后使用。

@include

当条件成立时,查询此字段

query {
    search {
        actors @include(if: $queryActor) {
            name
        }
    }
}
@skip

当条件成立时, 不查询此字段

query {
    search {
        comments @skip(if: $noComments) {
            from
        }
    }
}

今天要给大家介绍的是如何自定义指令(Directives)
实际用起来会像是下图所示

directive @isAuthenticated on FIELD_DEFINITION | OBJECT
directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @date(defaultFormat: String = "yyyy--MM-dd ") on FIELD_DEFINITION

type ExampleType @isAuthenticated {
  newField: String
  oldField: String @deprecated(reason: "use newField.")
  mobilePhoneNumber: String @length(max: 11)
  date: String @date
}

Directives 帮你实现TypeSystem做不到的细节
Directives 可视为GraphQL 的一种语法蜜糖(sugar syntax),通常用于调整query 及schema 的行为,不同场景下可以有以下功能:

  1. 影响query原有行为,如@include, @skip为query增加条件判断
  2. 为Schema加上描述性标签,如@deprecated可以用于废除schema的某field又避免breaking change
  3. 为Schema 添加新功能,例如参数检查、简单计算、权限检查、错误处理等等。不过这部分较为复杂,需自行定义

Directives 可以用在Client Side 的query 也可以用于Server Side 的Schema Definition ,不过通常比较多用于Schema Definition 中,一方面比较好维护,另一方面也减轻Client 的计算负担。

冷知识:通常在query使用的使用的Directives称为Executable Directive (或称Query Directive) ,在Schema中使用的称为Type System Directive (Schema Directive)。

1. 客户端 query + directives

GraphQL 在query side 原生支援的Executable Directive 有两个,分别为:

  1. @include (if: Boolean!): 用于判断是否显示此field,若if 为true 则显示。可用于field 及fragment 展开。
  2. @skip (if: Boolean!): 用于判断是否忽略此field,若if 为false 则显示。可用于field 及fragment 展开。
2. 服务端 schema definition + directives

前面提到Directives 可以为Schema 添加描述性标签或是添加新功能(或是两者兼具),所以我们先从添加描述性标签开始,介绍一下同样也是GraphQL 原生支持的Type System Directive @deprecated(reason: String = "No longer supported")

2.1 使用Type System Directives 标示Deprecated 范例

今天公司觉得某些男性/女性使用者并不喜欢透露自己的体重,所以决定废除这个栏位,但直接拿掉又怕系统出现问题,所以决定先dreprecate 掉,所以让我们修改Schema:

type User {
  ...
  "体重"
  weight(unit: WeightUnit = KILOGRAM): Float @deprecated (reason: "It's secret")
}

不过需注意,deprecated不代表说Client Side不能query,而是Client Side在阅读documentation时,会发现此field已经deprecated ,进而减少使用或修正目前的使用
如图所示,只要Server的Schema与Resolver并未移除掉该field ,User.weight仍然能取得值,只是在documentation中会将User.weight归类为Deprecated


image.png
3 服务端 schema definition + 自定义 Directives

举一个简单的例子,实作一个@uppper的Directives让回传的String都以大写形式呈现,我们需要做的有:

  1. 引入外部套件
const { SchemaDirectiveVistor } = require('apollo-server');
const { defaultFieldResolver } = require('graphql');
  1. 新增一个继承SchemaDirectiveVistor的class UpperCaseDirective来实作此Directive (可视为Directive的Resolver) ,再来通过override SchemaDirectiveVistor里的相关function来做出想要的效果。

这边@upper只针对栏位,因此只需要实作visitFieldDefinition。

  1. 将步骤2新增的class放入ApolloServer初始化的参数列在option添加新栏位schemaDirectives来将以上两者连接。
  2. 在Schema中( gqltag里)定义新的Directivesdirective @upper on FIELD_DEFINITION

代码如下:

// 1. 引入外部套件
const { ApolloServer, gql, SchemaDirectiveVisitor } = require('apollo-server');
const { defaultFieldResolver } = require('graphql');

// 2. Directive 實作
class UpperCaseDirective extends SchemaDirectiveVisitor {
  // 2-1. ovveride field Definition 的實作
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    // 2-2. 更改 field 的 resolve function
    field.resolve = async function(...args) {
      // 2-3. 取得原先 field resolver 的計算結果 (因為 field resolver 傳回來的有可能是 promise 故使用 await)
      const result = await resolve.apply(this, args);
      // 2-4. 將得到的結果再做預期的計算 (toUpperCase)
      if (typeof result === 'string') {
        return result.toUpperCase();
      }
      // 2-5. 回傳最終值 (給前端)
      return result;
    };
  }
}

// 3. 定義新的 Directive
const typeDefs = gql`
  directive @upper on FIELD_DEFINITION

  type Query {
    hello: String @upper
  }
`;

// Provide resolver functions for your schema fields
const resolvers = {
  Query: {
    hello: (root, args, context) => {
      return 'Hello world!';
    }
  }
};

// 4. Add directive to the ApolloServer constructor
const server = new ApolloServer({
  typeDefs,
  resolvers,
  // 4. 將 schema 的 directive 與實作連接並傳進 ApolloServer。
  schemaDirectives: {
    upper: UpperCaseDirective
  }
});

server.listen().then(({ url }) => {
  console.log(`? Server ready at ${url}`);
});

这边特别解释一下因为UpperCaseDirective在宣告时( directive @upper on FIELD_DEFINITION)只应用于FIELD_DEFINITION,所以我们只需要override visitFieldDefinition一项,若想知道其他Type要override的function的话,可见Apollo官方提供的API如以下程式码:

class SomeDirective extends SchemaDirectiveVisitor {
  visitSchema(schema: GraphQLSchema) {}

  visitObject(object: GraphQLObjectType) {}

  visitFieldDefinition(field: GraphQLField<any, any>) {}

  visitArgumentDefinition(argument: GraphQLArgument) {}

  visitInterface(iface: GraphQLInterfaceType) {}

  visitInputObject(object: GraphQLInputObjectType) {}

  visitInputFieldDefinition(field: GraphQLInputField) {}

  visitScalar(scalar: GraphQLScalarType) {}

  visitUnion(union: GraphQLUnionType) {}

  visitEnum(type: GraphQLEnumType) {}

  visitEnumValue(value: GraphQLEnumValue) {}
}

更多范例可以上Apollo Server 2 - Implementing directives上查询,另外很多directive的实作都可以找到套件只要下载来传入ApolloServer就ok了!

他强大的地方还有可以做ACL (Access Control List)也就是权限管理,比如今天Query的me的资料只能给有登入的使用而不开放给没有登入的guest,或是me.age只能给朋友看到,甚至是只有管理者( Admin可以删除使用者)

type User {
  ...
  @friendOnly
  age
}

Query {
  @isAuthenticated
  me: User
}

type Mutation {
  @auth(requires: "ADMIN")
  deleteUser(id: ID!): User
}

让我们来用之前社交软体的例子来试试看@isAuthenticated吧!

4. @isAuthenticated 实现

一样分为两部分: Schema与Resovler ( IsAuthenticatedDirectiveclass)

4.1 @isAuthenticated- Schema Definition

Schema部分非常简单,而我们目前仅用于FIELD_DEFINITION上~

directive @isAuthenticated on FIELD_DEFINITION

type Query {
  me: User @isAuthenticated
}
4.2 @isAuthenticated- Resolver
class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    field.resolve = async function(...args) {
      const context = args[2];
      // 检查有沒有 context.me
      if (!context.me) throw new ForbiddenError('Not logged in~.');

      // 确定有 context.me 后才进入 Resolve Function
      const result = await resolve.apply(this, args);
      return result;
    };
  }
}

const resolvers = {
  Query: {
    // 这里被纯做资料存取逻辑
    me: (root, args, { me, userModel }) => userModel.findUserByUserId(me.id),
    ...
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives: {
    // 一样要记得放入 ApolloServer 中
    isAuthenticated: IsAuthenticatedDirective
  }
});

虽然Directive 功能强大,但目前Directive 的应用还不算广且还有许多改进空间(实作难度偏高),所以可以审视自身需求来判断是否真的要新增一个Directive

Reference:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353