今天來跟大家介紹 GraphQL 的一個較少被人用到但十分好用的技巧: Diretives。比較常用於修飾 Schema 的定義來實現一些額外的功能或檢查。實際用起來會像是下圖所示。
使否開始有點感受到 Directives 的強大呢!讓我們接著看!
Directives
可視為 GraphQL 的一種語法蜜糖 (sugar syntax),通常用於調整 query 及 schema 的行為,不同場景下可以有以下功能
@include
, @skip
為 query 增加條件判斷@deprecated
可以用於廢除 schema 的某 field 又避免 breaking changeDirectives 可以用在 Client Side 的 query 也可以用於 Server Side 的 Schema Definition ,不過通常比較多用於 Schema Definition 中,一方面比較好維護,另一方面也減輕 Client 的計算負擔。
冷知識:通常在 query 使用的使用的 Directives 稱為 Executable Directive (或稱 Query Directive) , 在 Schema 中使用的稱為 Type System Directive (Schema Directive)。可以看 GraphQL Spec 裡面 (下圖)關於兩者的應用範圍,可以說 Directive 可以應用在任何地方只是實作難度的高地而已~
(圖片來源: https://facebook.github.io/graphql/June2018/#sec-Type-System.Directives)
GraphQL 在 query side 原生支援的 Executable Directive 有兩個(也比較常用),分別為
@include (if: Boolean!)
: 用於判斷是否顯示此 field,若 if 為 true 則顯示。可用於 field 及 fragment 展開。@skip (if: Boolean!)
: 用於判斷是否忽略此 field,若 if 為 false 則顯示。可用於 field 及 fragment 展開。回到我們的社交 Server API 例子 (可點選練習傳送門):
我可以做出以下操作:
# 因為 skip 與 include 的參數都是必填 `!` ,所以在 Variable Definition 時也要加上 `!`
query ($includeFriends: Boolean!, $skipSensitiveData: Boolean!) {
me {
name
...sensitiveData @skip(if: $skipSensitiveData)
friends @include(if: $includeFriends) {
id
}
}
}
fragment sensitiveData on User {
age
weight
height
}
----
Varaibles
{
"includeFriends": false,
"skipSensitiveData": true
}
請大家先思考一下會出現哪些資料呢? 想好後往下看
{
"data": {
"me": {
"name": "Fong"
}
}
}
答案是只會出現 name
而已 XD ,不過當你調整 Variables 從 false,true
成 true, false
後就會變成以下:
在這邊只要透過參數調整, directive
就可以有效減少前端 query 的數量也降低管理的負擔!
前面提到 Directives 可以為 Schema 添加描述性標籤或是添加新功能 (或是兩者兼具),所以我們先從添加描述性標籤開始,介紹一下同樣也是 GraphQL 原生支援的 Type System Directive @deprecated (reason: String = "No longer supported")
今天公司覺得某些男性/女性使用者並不喜歡透露自己的體重,所以決定廢除這個欄位,但直接拿掉又怕系統出現問題,所以決定先 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 。
舉一個簡單的例子,實作一個 @uppper
的 Directives 讓回傳的 String 都以大寫形式呈現,我們需要做的有
const { SchemaDirectiveVistor } = require('apollo-server');
const { defaultFieldResolver } = require('graphql');
SchemaDirectiveVistor
的 class UpperCaseDirective
來實作此 Directive (可視為 Directive 的 Resolver) ,再來透過 override SchemaDirectiveVistor
裡的相關 function 來做出想要的效果。@upper
只針對欄位,因此只需要實作 visitFieldDefinition
。ApolloServer
初始化的參數列 在 option 添加新欄位 schemaDirectives
來將以上兩者連接。gql
tag 裡) 定義新的 Directives directive @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
吧 !
@isAuthenticated
實作一樣分為兩部分: Schema 與 Resovler (IsAuthenticatedDirective
class)
@isAuthenticated
- Schema DefinitionSchema 部分非常簡單,而我們目前僅用於 FIELD_DEFINITION
上~
directive @isAuthenticated on FIELD_DEFINITION
type Query {
me: User @isAuthenticated
}
@isAuthenticated
- Resolver接著來定義 IsAuthenticatedDirective
class 。
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
}
});
@isAuthenticated
- Demo接著就讓我們 Demo 看看!來個不帶 token
的 me
試試看~結果如下圖
成功!
延伸閱讀: Apollo Draft specification for GraphQL Schema Decorators
@directive
type User {
firstName: String
lastName: String
fullName: String @computed(value: "$firstName $lastName")
}
type Query {
me: User
}
;
Date
Scalar Type ,不過也可以透過 directive 來做更彈性的時間格式管理。directive @date(format: String) on FIELD_DEFINITION
scalar Date
type Post {
published: Date @date(format: "mmmm d, yyyy")
}
或甚至把 format 交給 Client 決定!
directive @date(
defaultFormat: String = "mmmm d, yyyy"
) on FIELD_DEFINITION
scalar Date
type Query {
today: Date @date
}
directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
type Query {
books: [Book]
}
type Book {
title: String @length(max: 50)
}
type Mutation {
createBook(book: BookInput): Book
}
input BookInput {
title: String! @length(max: 50)
}
其實 Apollo 還有另一種 directiveResolvers
的作法,有點類似 middleware 的概念,實作起來雖然功能較少但更簡單!不過 Apollo 未來不一定會繼續支援,所以要用的請小心 QQ ,可參考 這篇
雖然 Directive 功能強大,但目前 Directive 的應用還不算廣且還有許多改進空間 (實作難度偏高),所以可以審視自身需求來判斷是否真的要新增一個 Directive 。
而自定義 Directive 目前在 Schema Defintion 比較常見 (如 SchemaDirectiveVisitor
字面所言),在 query 方面比較少見,以 Apollo 官方文件說法 來看,他們認為 Directive 比較適合用於 Schema 這邊,一方面比較好維護 Directive ,另一方面也可以減少 Client 的計算負擔,不過 Apollo 也說如果真的有這類需求他們也會考慮未來實作出來 (其實現在就有實驗性質的 Query Directive @defer
讓 Client 在請求大量資料時可以延遲取得,可參考這篇。
Reference:
@fx777
您好,在文中的範例4-2. @isAuthenticated - Demo有提到不帶 token 的 me 是不無法取的資料成功的。
想請教,要如何在GQL中攜帶token才能讓後端可以識別?