點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Apollo GraphQL 建立 Subscription Filter

Sam Xiao's Avatar 2019-12-29

實務上可能不想接收所有 Subscription,而是只想訂閱有興趣內容,此時可如 Query 對 Subscription 傳入 Argument,Apollo GraphQL 將使用 Subscription Filter 處理。

Version

macOS Catalina 10.15.2
WebStorm 2019.3.1
Node 12.4.0
Apollo GraphQL 2.9.6

Apollo GraphQL

src/index.js

import { ApolloServer, gql, PubSub, withFilter } from 'apollo-server'

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
]

let pubsub = new PubSub()

let typeDefs = gql`
  type Query {
    books: [Book]
  }
  
  type Mutation {
    addBook(book: BookInput!): Book
  } 

  type Subscription {
    newBook(title: String!): Book
  }

  type Book {
    title: String
    price: Int
  }

  input BookInput {
    title: String!
    price: Int!
  }
`

let books = () => data

let addBook = (_, { book }, { pubsub }) => {
  data.push(book)

  pubsub.publish('bookAdded', {
    newBook: book,
    payload: book.title
  })

  return book
}

let newBook = {
  subscribe: withFilter(
    (_, __, { pubsub }) => pubsub.asyncIterator('bookAdded'),
    ({ payload }, { title }) => payload === title
  )
}

let resolvers = {
  Query: {
    books
  },
  Mutation: {
    addBook
  },
  Subscription: {
    newBook
  }
}

let context = () => ({ pubsub })

new ApolloServer({ typeDefs, resolvers, context })
  .listen()
  .then(({ url }) => `GraphQL Server ready at ${ url }`)
  .then(console.log)

第 1 行

import { ApolloServer, gql, PubSub, withFilter } from 'apollo-server'

若要使用 subscription,除了 import ApolloServergql 外,還需要 PubSub

withFilter() 則是 subscription filter 所使用,用來過濾 event 所傳來資料,只將 user 所關注資料回傳。

第 3 行

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
]

原始只有 2 筆資料,第 3 筆將由 mutation 新增並通知 subscription。

第 8 行

let pubsub = new PubSub()

PubSub 為 factory class,實作了 PubSubEngine interface,稍後會以 publish() 發布 event。

11 行

type Query {
  books: [Book]
}
  
type Mutation {
  addBook(book: BookInput!): Book
} 

type Subscription {
  newBook(title: String!): Book
}

type Book {
  title: String
  price: Int
}

input BookInput {
  title: String!
  price: Int!
}

在 schema 宣告 books query、addBook mutation 與 newBook subscription,值得注意的是 subscription 宣告方式與 query 與 mutation 並無差異,且多了 title argument。

54 行

let resolvers = {
  Query: {
    books
  },
  Mutation: {
    addBook
  },
  Subscription: {
    newBook
  }
}

resolvers 宣告 books query、addBook mutation 與 newBook subscription,值得注意的是 subscription 宣告方式與 query 與 mutation 並無差異。

34 行

let books = () => data

實踐 books query,目前只是回傳所有資料。

36 行

let addBook = (_, { book }, { pubsub }) => {
  data.push(book)

  pubsub.publish('bookAdded', {
    newBook: book,
    payload: book.title
  })

  return book
}

實踐 addBook mutation,重點在 mutation 內使用 pubsub.publish() 發布 bookAdded event。

pubsub 直接從 context argument 解構,稍後會建立 context。

publish() 的第一個 argument 為 event 名稱。

第二個 argument 為要發到 event 的資料:

  • newBook 為 subscription 名稱,book 為要傳給 subscription 的資料
  • payload 為使用 subscription filter 時的 argument 名稱,book.title 為傳給 filter 資料,若不使用 filter 可省略不傳 payload

47 行

let newBook = {
  subscribe: withFilter(
    (_, __, { pubsub }) => pubsub.asyncIterator('bookAdded'),
    ({ payload }, { title }) => payload === title
  )
}

實踐 newBook subscription,實作 subscribe() 時特別使用 withFilter() higher order function。

withFilter() 第一個 argument 為 simple subscription 的 subscribe(),由 context argument 直接解構出 pubsub,再由 pubsub.asyncIterator() 訂閱 bookAdded event。

第二個 argument 相當於 subscription filter 的 predicate,回傳 truefalse,由 第一個 argument 解構出從 addBook mutation 傳來的 payload,再由第二個 argument 解構出從 newBook subscription 傳來的 title

66 行

let context = () => ({ pubsub })

new ApolloServer({ typeDefs, resolvers, context })
  .listen()
  .then(({ url }) => `GraphQL Server ready at ${ url }`)
  .then(console.log)

由於各 resolver 都要使用 pubsub,因此特別將 pubsub 放到 context 內。

context() 內回傳包含 pubsub 的 object,然後再 new ApolloServer 時傳入。

GraphQL Playground

Query

query {
  books {
    title
    price
  }
}

執行 books query 回傳目前所有資料,共兩筆。

filter000

Subscription

subscription {
  newBook(title: "Speaking JavaScript") {
    title
    price
  }
}

執行 newBook subscription,且只有當 titleSpeaking JavaScript 才通知,由於還沒被觸發,因此處在 listening 狀態。

filter001

Mutation

mutation {
  addBook(book: {
    title: "JavaScript Ninja",
    price: 400
  }) {
    title
    price
  }
}

執行 addBook mutation,新增 titleJavaScript Ninja 的 book。

filter002

Subscription

subscription {
  newBook(title: "Speaking JavaScript") {
    title
    price
  }
}

可發現 newBook subscription 並沒有被觸發,因為 title 並不是 Speaking JavaScript

filter003

Mutation

mutation {
  addBook(book: {
    title: "Speaking JavaScript",
    price: 500
  }) {
    title
    price
  }
}

執行 addBook mutation,新增 titleSpeaking JavaScript 的 book,此 mutation 將觸發 newBook subscription。

filter004

Subscription

subscription {
  newBook(title: "Speaking JavaScript") {
    title
    price
  }
}

原本 newBook subscription 由 listening 得到回傳資料。

filter005

Conclusion

  • Subscription filter 關鍵在於 withFilter() higher order function,第一個 argument 與普通 subscription 無異,目的只是訂閱 event,第二個 argument 則是 predicate,可比對 event 傳來的資料與 subscription 傳來的資料
  • Subscription 會在 resolver 內使用 pubsub,因此特別適合將 pubsub 放在 context 內讓多個 resolver 共用

Sample Code

完整範例可在我的 GitHub 上找到

Reference

Apollo, Subscription Filters
Apollo, Adding Subscriptions To Schema
Shadaj Laddad, Tutorial: GraphQL Subscriptions on the Server
Alex Banks & Eve Porcello, Learning GraphQL