Custom Payload Types

Copy Markdown View Source

This guide covers the usage of custom payload types.

When to Use

One of the many benefits of Ecto is the strict separation of database data and application data. While PGMQ requires JSONB payloads for messages, EctoPGMQ allows applications to use any term/0 as a message payload as long as there is a corresponding Ecto.Type or Ecto.ParameterizedType implementation that dumps to and loads from a map/0.

Custom payload types are supported by all EctoPGMQ functions that read or write PGMQ messages.

Multiple Payload Types in a Queue

There are no guardrails in place to ensure that a single payload type is used for a given queue. Mixing multiple payload types in a single queue only works if they can all be loaded interchangably. This pattern is NOT recommended.

Motivating Example

Assume that an application supports webhooks and processes them with a queue. The following Ecto.Type implementation can be used to read and write messages with a Webhook struct as a payload:

defmodule MyApp.Webhook do
  @moduledoc false

  use Ecto.Type

  defstruct [:url, :body, :code]

  @doc false
  @impl Ecto.Type
  def cast(%__MODULE__{} = webhook), do: {:ok, webhook}

  @doc false
  @impl Ecto.Type
  def dump(%__MODULE__{url: url, body: body, code: code}) do
    {:ok, %{"body" => body, "url" => URI.to_string(url), "code" => code}}
  end

  @doc false
  @impl Ecto.Type
  def load(%{"body" => body, "url" => url} = data) do
    {:ok, %__MODULE__{body: body, code: data["code"], url: URI.new!(url)}}
  end

  @doc false
  @impl Ecto.Type
  def type, do: :map
end

The examples below illustrate some of the ways that the above implementation can be used in practice:

# Create a webhook queue
EctoPGMQ.create_queue(MyApp.Repo, "webhook_queue")

# Send a webhook to the queue
webhook = %MyApp.Webhook{
  code: nil,
  body: %{"data" => 123_456},
  url: URI.new!("https://host:443/path?foo=bar")
}

message = EctoPGMQ.Message.build(webhook)
EctoPGMQ.send_messages(MyApp.Repo, "webhook_queue", [message], payload_type: MyApp.Webhook)

# Read a webhook from the queue
messages = EctoPGMQ.read_messages(MyApp.Repo, "webhook_queue", 300, 1, payload_type: MyApp.Webhook)
[%EctoPGMQ.Message{payload: %MyApp.Webhook{}}] = messages

# Query for webhooks directly
messages =
  "webhook_queue"
  |> EctoPGMQ.Message.queue_query(payload_type: MyApp.Webhook)
  |> MyApp.Repo.all()

[%EctoPGMQ.Message{payload: %MyApp.Webhook{}} | _] = messages

Payload Filtering

Due to how custom payload types are applied, non-map payloads can't be directly interpolated in an Ecto.Query. Instead, payloads must be cast with Ecto.Query.API.type/2:

"webhook_queue"
|> EctoPGMQ.Message.queue_query(payload_type: MyApp.Webhook)
|> where([m], m.payload == type(^webhook, MyApp.Webhook))

Ecto Type

Note that the second argument to the aforementioned function is NOT a EctoPGMQ.Message.payload_type/0 but rather an Ecto.Type.t/0.

Alternatively, PGMQ has experimental support for payload filtering during reads. For more information, see EctoPGMQ.PGMQ.conditional/0.