iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0

在先前我們為了讓查詢使用上更加彈性,學習了使用變數(Variables)動態帶入引數(Arguments),接著我們學習指令(Directives),它就像是 GraphQL 中的魔法咒語一般,讓我們可以動態地改變查詢結果,像是動態改變查詢結果格式、改變查詢結果資料樣式或是一定程度上共用查詢語法。

指令(Directives)語法是@加上名稱,後面的括號內是帶入的引數與引數值,如下圖:
https://ithelp.ithome.com.tw/upload/images/20230925/20161957KzWPtab1WS.png

query UserQuery(
  $userId: Int!,
  $hideRole: Boolean! = false,
  $includeDetail: Boolean! = true
) {
  user(id: $userId) {
    username
    id
    email
    isActive
    fullName
    role @skip(if: $hideRole)
    detail @include(if: $includeDetail) {
      ... on UserProfile {
        address
      }
    }
  }
}
{
  "userId": {自行帶入可用的ID},
  "hideRole": true,
  "includeDetail": false
}

這邊新定義了兩個不可是 Null 的 Boolean 變數$hideRole$includeDetail,兩個變數都給了預設值,detail 內的 內嵌片段(Inline Fragments) 是一個UserProfile的介面(Interface),然後我們可以看到兩個指令:

  • @skip(if: Boolean!):如果符合條件就跳過該欄位或物件。
  • @include(if: Boolean!):如果符合條件就包含該欄位或物件。

以上這兩個指令是 GraphQL 預設內建的指令,接著我們使用 Strawberry 自定義自己的指令。

# app/__init__.py
import strawberry

+from graphql import DirectiveLocation

from app import query

__all__ = ["schema"]

+@strawberry.directive(
+   locations=[DirectiveLocation.FIELD],
+   name="sensitive",
+   description="Replace sensitive text with *",
+)
+def sensitive_text(value: str) -> str:
+   ignore_list = [" ", ",", ",", ".", "。", "!", "?", "!", "?"]
+   return "".join([i if i in ignore_list else "*" for i in value])

-schema = strawberry.Schema(query=query.Query)
+schema = strawberry.Schema(query=query.Query, directives=[sensitive_text])

https://ithelp.ithome.com.tw/upload/images/20230925/20161957k7oLU3tE3u.png

query UserQuery(
  $userId: Int!,
  $hideRole: Boolean! = false,
  $includeDetail: Boolean! = true
) {
  user(id: $userId) {
    username
    id
    email
    isActive
    fullName @sensitive
    role @skip(if: $hideRole)
    detail @include(if: $includeDetail) {
      ... on UserProfile {
        address
        phone @sensitive
        birthdate
      }
    }
  }
}

上面我們實作了一個名為@sensitive的指令用於讓客戶端可以自行決定哪些欄位是敏感文字,將結果的文字轉成*,在程式碼的部分我們定義一個函式sensitive_text,在 Strawberry 預設的情況下會使用函式名稱轉成駝峰式命名當成指令名稱,但是我們在@strawberry.directive的地方設定了名稱,並且設定作用位置是欄位。在函式內先定義一個忽略轉換的文字清單,最後一個字一個字檢查,不在忽略清單的文字就轉成*@sensitive是一個不需要引數的指令。

接著我們再練習一個帶有引數的指令:

# app/__init__.py
# ... 省略

+@strawberry.directive(locations=[DirectiveLocation.FIELD])
+def replace(value: str, old: str, new: str):
+    return value.replace(old, new)

-schema = strawberry.Schema(query=query.Query, directives=[sensitive_text])
+schema = strawberry.Schema(
+   query=query.Query,
+   directives=[sensitive_text, replace],
+)

https://ithelp.ithome.com.tw/upload/images/20230925/201619579CHOOxSKx9.png

query UserQuery($userId: Int!) {
  user(id: $userId) {
    username
    id
    email
    isActive
    fullName @sensitive @replace(old: "*", new: "#")
    role
    detail {
      ... on UserProfile {
        address
        phone @sensitive
        birthdate
      }
    }
  }
}

這邊我們新增了一個字串替換的指令,在字串欄位使用@replace(old: String, new: String),然後可以看到指令是可以多個一起使用,就如同 Linux 的 pipe 一樣,但是相同的指令無法在同一個位置重複使用,且指令的回傳型態要與原本相同。

在前面提到的指令(Directives)其實全名叫 操作指令(Operation Directives) ,顧名思義就是會影響執行過程的指令,會這麼說是因為還有另ㄧ種類型的指令,叫做 Schema 指令(Schema Directives) ,它就是在前面說明型別系統時,所提到的 Directive,主要功能是提供 Schema 額外資訊(Metadata)的描述。

接著來練習 Schema 指令,我們先幫 User 多加個欄位:

# app/types.py
# ... 省略
from strawberry import scalars

# ... 省略

@strawberry.type
class User:
    id: strawberry.ID
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
+    avatar: typing.Optional[scalars.Base64] = strawberry.field(
+       description="Base64 encoded avatar",
+       deprecation_reason="Removed this field.",
+       default=None,
+   )
# ... 省略

接著我們將 GraphQL Schema 匯出成檔案:

$ strawberry export-schema main:schema --output schema.graphql
Schema exported to schema.graphql

然後我們打開schema.graphql檔案來看,可以看到以下兩個特別的標註:

"""
Represents binary data as Base64-encoded strings, using the standard alphabet.
"""
scalar Base64 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-4")
type User {
  id: ID!
  username: String!
  email: String!
  firstName: String!
  lastName: String!
  password: String!

  """Base64 encoded avatar"""
  avatar: Base64 @deprecated(reason: "Removed this field.")
  lastLogin: DateTime

  """Is the user active?"""
  isActive: Boolean!
  role: UserRole!
  detail: UserDetail!
  fullName: String!
}

可以看到兩個 Schema 指令@specifiedBy@deprecated,這兩個指令都是內建指令:

  • @specifiedBy:用於標記客製化 Scalar 規格文件 URL。
  • @deprecated:用於標記棄用與棄用說明的欄位或是 Enum 值。

strawberry.scalars.Base64是 strawberry 幫我們預先建立好的客製化 Scalar。

Schema 指令 strawberry 也有提供自行客製化的功能,如有相關需求可以參考官方文件說明 [1]。

app/__init__.py完整程式碼:

import datetime
import typing
import strawberry

from graphql import DirectiveLocation

from app import query

__all__ = ["schema"]

@strawberry.directive(
    locations=[DirectiveLocation.FIELD],
    name="sensitive",
    description="Replace sensitive text with *",
)
def sensitive_text(value: str) -> str:
    ignore_list = [" ", ",", ",", ".", "。", "!", "?", "!", "?"]
    return "".join([i if i in ignore_list else "*" for i in value])

@strawberry.directive(locations=[DirectiveLocation.FIELD])
def replace(value: str, old: str, new: str):
    return value.replace(old, new)

schema = strawberry.Schema(
    query=query.Query,
    directives=[sensitive_text, replace],
)

app/types.py完整程式碼:

import datetime
import enum
import typing
import strawberry

from strawberry import scalars

@strawberry.enum
class UserRole(enum.Enum):
    NORMAL = strawberry.enum_value(
        "normal",
        description="Normal user",
    )
    STAFF = "staff"
    MANAGER = "manager"
    ADMIN = "admin"

@strawberry.interface
class UserProfile:
    phone: str
    birthdate: datetime.date
    address: typing.Optional[str]

@strawberry.type
class NormalUserDetail(UserProfile):
    pass

@strawberry.type
class StaffUserDetail(UserProfile):
    department: str

@strawberry.type
class ManagerUserDetail(StaffUserDetail):
    subordinates: typing.List["User"]

@strawberry.type
class AdminUserDetail:
    system_permissions: typing.List[str]

@strawberry.type
class User:
    id: strawberry.ID
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
    avatar: typing.Optional[scalars.Base64] = strawberry.field(
        description="Base64 encoded avatar",
        deprecation_reason="Removed this field.",
        default=None,
    )
    last_login: typing.Optional[datetime.datetime]
    is_active: bool = strawberry.field(
        default=True,
        description="Is the user active?",
    )
    role: UserRole
    detail: typing.Annotated[
        typing.Union[
            NormalUserDetail,
            StaffUserDetail,
            ManagerUserDetail,
            AdminUserDetail,
        ],
        strawberry.union("UserDetail"),
    ]

    @strawberry.field
    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

參考資料


上一篇
Day 9:使用 Strawberry 學習 GraphQL 片段與內嵌片段
下一篇
Day 11:使用 Strawberry 學習 GraphQL 變更
系列文
Django 與 Strawberry GraphQL:探索現代 API 開發之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言