今天想要介紹 Mutation 的一些設計上的習慣與技巧!
mutation 的設計越簡潔越好,以最少的參數來實現功能,比如一些動詞如 delete
, publish
(上架), unpublish
、activate
等等可能僅需一個 id
或 ids
就可以滿足需求,
因此設計出來的 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)
}
不過以上的 create
與 update
的參數似乎多到有點難以管理,所以如 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
}
這邊建議,如果輸入的格式模糊 (可能值很多)且前端驗證相對簡單,但就很適合使用 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
}
在設計 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 ,但對於未來擴充性以及管理性都有不錯的效果!
在這邊另外介紹 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