iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 25
1

header

今天想要介紹 Mutation 的一些設計上的習慣與技巧!


1. Mutation Input 設計

mutation 的設計越簡潔越好,以最少的參數來實現功能,比如一些動詞如 delete, publish (上架), unpublishactivate 等等可能僅需一個 idids 就可以滿足需求,

因此設計出來的 query 可能會如下:

type Mutation {
  deleteProduct(productId: ID!)
  removeProductFromOrder(proudctId: ID!)
  publishProudcts(productIds: [ID!]!)

  createProduct(variants: [ProductVariants!]!, price: Int, stock: Int, image: Image, description: String)
  updateProduct(id: ID!, variants: [ProductVariants!]!, price: Int, stock: Int, image: Image, description: String)
}

不過以上的 createupdate 的參數似乎多到有點難以管理,所以如 create, update 我們通常會多新增一個 input object type 來支援參數。

type Mutation {
  deleteProduct(productId: ID!)
  removeProductFromOrder(proudctId: ID!)
  publishProudcts(productIds: [ID!]!)

  createProduct(product: ProductInput)
  updateProduct(id: ID!, product: ProductInput)
}

type ProductInput {
  variants: [ProductVariants!]!
  price: Int
  stock: Int
  image: Image
  description: String
}

Input Scalar Type 如何選擇 ?

這邊建議,如果輸入的格式模糊 (可能值很多)且前端驗證相對簡單,但就很適合使用 Strong Type (也就是使用 custom scalar type) 。比如 Date 格式,前端可能輸入千百種格式 (Unix timestamp 、 ISO 、 純日期...) ,這時候限制前端輸入的 Date 格式,而前端也只需要做個日期選擇器,就避免了很多不必要的驗證與檢查。

但另一方面,輸入的格式若是足夠清楚但前端驗證相對複雜,就建議用 Weaker Type ,讓後端去跑驗證程序。比如要輸入 CSV 格式字串 (a,b,c,d,e) ,就建議直接用 String ,接下來交給後端去檢查,一方面後端可以做更仔細的檢查,另一方面也避免前後端個自維護檢查邏輯的程式。

PS. Shopify 將 email 也歸類於「格式清楚、前端驗證複雜」,但我個人認為讓前端先檢查好 email 會讓使用者體驗比較好一點。

type Mutation {
  updateUserInfo(userId: ID!, input: UserInput): UpdateUserInfoPayload
  updateActivityTable(
    "CSV 格式的字串"
    table: String
  ): UpdateActivityTablePayload
}

type {
  email: Email
  birthDay: Date
}

2. Mutation Output 設計

在設計 mutation output type 時,最簡單的就是直接回傳被更改的 type ,如 updateOrder 就應該回傳 Order type 更新後的資料,但軟體的設計常常跟不上需求,有可能今天需要增加錯誤處理邏輯或是提供更多 mutation 的詳細資訊如成功/失敗次數時,只用一個現成的 object type 就有了擴展的困難。

因此,可以考慮為每一個 mutation 回傳資料都設計一個專屬的 object type ,比如一個 updateOrder 就可以多新增一個 UpdateOrderPayload

type Mutation {
  updateOrder(id: ID!, input: UpdateOrderInput): UpdateOrderPayload
}

type UpdateOrderPayload {
  updatedOrder: Order
  "商業邏輯層的錯誤"
  error: [UserError!]!
  "執行時間"
  timestamp: Date
}

type UserError {
  code: String
  message: String!

  # Path to input field which caused the error.
  field: [String!]
}

這邊 shopify 的建議可以定義一個 userErrors 來表達商業邏輯層的錯誤,而那種 data 層級的 error 比較適合留個一些非商業邏輯層的錯誤,如前端發送的 query 不合法、後端驗證不通過等,而 [UserError!]! 保證就算沒有錯誤也會回傳空 array。

另外 payload 裡面大部分的欄位建議不要加上 non-null 以免造成如之前所說限制了 schema 的發展。

經驗談: 為 mutation 特別設計回傳 object type 的做法真的很推薦,如此一來每次新增需求時,前後端也不必卡住對方進度來互相配合。雖然剛開始需要花費精力新增許多看似大才小用的 object type ,但對於未來擴充性以及管理性都有不錯的效果!

3. Relay Mutation 設計

在這邊另外介紹 Relay Input Object Mutations Specification

與前面不同的是,遵守 relay mutation 的 spec 會為每支 mutation 新增一個專屬的 input object type 與一個專屬的回傳 object type (與前面相同)。

type Mutation {
  deleteProduct(input: DeleteProductInput!): DeleteProductPayload
  removeProductFromOrder(input: RemoveProductFromOrderInput!): RemoveProductFromOrderPayload
  publishProudcts(input: PublishProudctsInpu!): PublishProudctsPayload

  createProduct(input; CreateProductInput!): CreateProductPayload
  updateProduct(input: UpdateProductInput!): UpdateProductPayload
}

input DeleteProductInput {
  "紀錄哪個 client 發送的"
  clientMutationId: ID!
  productId: ID
}

type DeleteProductPayload {
  clientMutationId: ID!
  deletedProduct: Product
}

同樣可以參考 GitHub API Explorer 裡面關於 mutation 的設計~


Reference:

https://medium.com/graphql-mastery/graphql-best-practices-for-graphql-schema-design-91fcab4dec0a


上一篇
Think in GraphQL - Schema Mutation 設計守則 - 1
下一篇
GraphQL 前端: Apollo Client 攜手 React 擁抱 GraphQL
系列文
Think in GraphQL30

尚未有邦友留言

立即登入留言