snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s

This commit is contained in:
upstream-mirror
2026-09-15 21:27:46 +05:00
commit ce933d1277
538 changed files with 46504 additions and 0 deletions

38
tools/pq/README.md Normal file
View File

@@ -0,0 +1,38 @@
# pq
pq is a CLI tool for working with delayed messages on poison queues.
For now, it supports the PostgreSQL Pub/Sub implementation.
## Install
```bash
go install github.com/ThreeDotsLabs/watermill/tools/pq@latest
```
## Usage
Set the `DATABASE_URL` environment variable to your PostgreSQL connection string.
For example, to connect to the database used for the [delayed requeue example](../../_examples/real-world-examples/delayed-requeue):
```bash
export DATABASE_URL="postgres://watermill:password@postgres:5432/watermill?sslmode=disable"
```
```bash
pq -backend postgres -topic requeue
```
This will use the default `watermill_` prefix, so will use the `watermill_requeue` table.
If you use a custom prefix, use the `-raw-topic` flag instead:
```bash
pq -backend postgres -raw-topic my_prefix_requeue
```
## Commands
- Requeue — Updates the `_watermill_delayed_until` metadata to the current time, so the message will be instantly requeued.
- Ack — Removes the message from the queue (be careful — you will lose the message forever).

View File

@@ -0,0 +1,99 @@
package backend
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/ThreeDotsLabs/watermill/components/delay"
"github.com/ThreeDotsLabs/watermill/tools/pq/cli"
)
type PostgresMessage struct {
Offset int `db:"offset"`
UUID string `db:"uuid"`
Payload string `db:"payload"`
Metadata string `db:"metadata"`
}
type PostgresBackend struct {
db *sqlx.DB
config cli.BackendConfig
}
func NewPostgresBackend(ctx context.Context, config cli.BackendConfig) (*PostgresBackend, error) {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
return nil, fmt.Errorf("missing DATABASE_URL")
}
db, err := sqlx.Connect("postgres", dbURL)
if err != nil {
return nil, err
}
return &PostgresBackend{
db: db,
config: config,
}, nil
}
func (r *PostgresBackend) AllMessages(ctx context.Context) ([]cli.Message, error) {
var dbMessages []PostgresMessage
err := r.db.SelectContext(ctx, &dbMessages, fmt.Sprintf(`SELECT "offset", uuid, payload, metadata FROM %v WHERE acked = false ORDER BY "offset"`, r.topic()))
if err != nil {
return nil, err
}
var messages []cli.Message
for _, dbMsg := range dbMessages {
var metadata map[string]string
err := json.Unmarshal([]byte(dbMsg.Metadata), &metadata)
if err != nil {
return nil, err
}
msg, err := cli.NewMessage(fmt.Sprint(dbMsg.Offset), dbMsg.UUID, dbMsg.Payload, metadata)
if err != nil {
return nil, err
}
messages = append(messages, msg)
}
return messages, nil
}
func (r *PostgresBackend) Requeue(ctx context.Context, msg cli.Message) error {
_, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET metadata = metadata::jsonb || jsonb_build_object($1::text, $2::text) WHERE "offset" = $3`, r.topic()),
delay.DelayedUntilKey, time.Now().UTC().Format(time.RFC3339), msg.ID,
)
if err != nil {
return err
}
return nil
}
func (r *PostgresBackend) Ack(ctx context.Context, msg cli.Message) error {
_, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET acked = true WHERE "offset" = %v`, r.topic(), msg.ID))
if err != nil {
return err
}
return nil
}
func (r *PostgresBackend) topic() string {
if r.config.Topic != "" {
return fmt.Sprintf(`"watermill_%v"`, r.config.Topic)
}
return fmt.Sprintf(`"%v"`, r.config.RawTopic)
}

32
tools/pq/cli/backend.go Normal file
View File

@@ -0,0 +1,32 @@
package cli
import (
"context"
"github.com/pkg/errors"
)
type BackendConfig struct {
Topic string
RawTopic string
}
func (c BackendConfig) Validate() error {
if c.Topic == "" && c.RawTopic == "" {
return errors.New("topic or raw topic must be provided")
}
if c.Topic != "" && c.RawTopic != "" {
return errors.New("only one of topic or raw topic must be provided")
}
return nil
}
type BackendConstructor func(ctx context.Context, cfg BackendConfig) (Backend, error)
type Backend interface {
AllMessages(ctx context.Context) ([]Message, error)
Requeue(ctx context.Context, msg Message) error
Ack(ctx context.Context, msg Message) error
}

45
tools/pq/cli/message.go Normal file
View File

@@ -0,0 +1,45 @@
package cli
import (
"time"
"github.com/ThreeDotsLabs/watermill/components/delay"
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
)
type Message struct {
// ID is a unique message ID across the Pub/Sub's topic.
ID string
UUID string
Payload string
Metadata map[string]string
OriginalTopic string
DelayedUntil string
DelayedFor string
RequeueIn time.Duration
}
func NewMessage(id string, uuid string, payload string, metadata map[string]string) (Message, error) {
originalTopic := metadata[middleware.PoisonedTopicKey]
// Calculate the time until the message should be requeued
delayedUntil, err := time.Parse(time.RFC3339, metadata[delay.DelayedUntilKey])
if err != nil {
return Message{}, err
}
delayedFor := metadata[delay.DelayedForKey]
requeueIn := delayedUntil.Sub(time.Now().UTC()).Round(time.Second)
return Message{
ID: id,
UUID: uuid,
Payload: payload,
Metadata: metadata,
OriginalTopic: originalTopic,
DelayedUntil: delayedUntil.String(),
DelayedFor: delayedFor,
RequeueIn: requeueIn,
}, nil
}

403
tools/pq/cli/model.go Normal file
View File

@@ -0,0 +1,403 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"slices"
"time"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/exp/maps"
)
var warningStyle = lipgloss.NewStyle().
Background(lipgloss.Color("196")).
Align(lipgloss.Center).
Padding(1, 10)
var dialogStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("241")).
Padding(1, 4)
var buttonStyle = lipgloss.NewStyle()
var buttonSelectedStyle = lipgloss.NewStyle().
Background(lipgloss.Color("57"))
var readOnlyMessageActions = []string{"<- Back", "Show payload"}
var writeMessageActions = []string{"Requeue", "Ack (drop)"}
var dialogActions = []string{"Cancel", "Confirm"}
type MessagesUpdated struct {
Messages []Message
}
type DialogResult struct {
Err error
}
func (m Model) FetchMessages() tea.Cmd {
return func() tea.Msg {
for {
msgs, err := m.backend.AllMessages(context.Background())
if err != nil {
panic(err)
}
m.sub <- MessagesUpdated{
Messages: msgs,
}
time.Sleep(time.Second)
}
}
}
func (m Model) WaitForMessages() tea.Cmd {
return func() tea.Msg {
return <-m.sub
}
}
var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
type Model struct {
backend Backend
sub chan MessagesUpdated
chosenMessage *Message
chosenMessageGone bool
table table.Model
messages []Message
chosenAction int
currentDialog *Dialog
showingPayload bool
payloadViewport viewport.Model
}
func (m Model) Init() tea.Cmd {
return tea.Batch(
m.FetchMessages(),
m.WaitForMessages(),
)
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case MessagesUpdated:
rows := make([]table.Row, len(msg.Messages))
for i, message := range msg.Messages {
rows[i] = table.Row{
message.ID,
message.UUID,
message.OriginalTopic,
message.DelayedFor,
message.RequeueIn.String(),
}
}
m.table.SetRows(rows)
m.messages = msg.Messages
// If the chosen message is no longer in the list, go back to the table.
// This is to avoid accidentally making an action on a message that has been requeued or deleted.
if m.chosenMessage != nil {
found := false
for _, message := range m.messages {
if message.ID == m.chosenMessage.ID {
foundMessage := message
m.chosenMessage = &foundMessage
found = true
break
}
}
if found {
m.chosenMessageGone = false
} else {
if !m.chosenMessageGone {
m.chosenAction = 0
}
m.chosenMessageGone = true
}
}
return m, m.WaitForMessages()
}
if m.chosenMessage == nil {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case " ", "enter":
c := m.table.Cursor()
m.chosenAction = 0
chosenMessage := m.messages[c]
m.chosenMessage = &chosenMessage
m.chosenMessageGone = false
}
}
var cmd tea.Cmd
m.table, cmd = m.table.Update(msg)
return m, cmd
} else if m.showingPayload {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc", "backspace":
m.showingPayload = false
}
}
var cmd tea.Cmd
m.payloadViewport, cmd = m.payloadViewport.Update(msg)
return m, cmd
} else if m.currentDialog != nil {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc", "backspace":
m.currentDialog = nil
case "h", "left":
m.currentDialog.Choice--
if m.currentDialog.Choice < 0 {
m.currentDialog.Choice = 0
}
case "l", "right":
m.currentDialog.Choice++
if m.currentDialog.Choice >= len(dialogActions) {
m.currentDialog.Choice = len(dialogActions) - 1
}
case " ", "enter":
switch m.currentDialog.Choice {
case 0:
m.currentDialog = nil
case 1:
m.currentDialog.Running = true
return m, m.currentDialog.Action
}
}
case DialogResult:
if msg.Err != nil {
// TODO Could be handled better
panic(msg.Err)
}
m.currentDialog = nil
}
return m, nil
} else {
messageActions := len(readOnlyMessageActions)
if !m.chosenMessageGone {
messageActions += len(writeMessageActions)
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc", "backspace":
m.chosenMessage = nil
m.chosenMessageGone = false
case "j", "down":
m.chosenAction++
if m.chosenAction >= messageActions {
m.chosenAction = messageActions - 1
}
case "k", "up":
m.chosenAction--
if m.chosenAction < 0 {
m.chosenAction = 0
}
case " ", "enter":
switch m.chosenAction {
case 0:
m.chosenMessage = nil
m.chosenMessageGone = false
case 1:
// Show payload
m.showingPayload = true
m.payloadViewport = viewport.New(80, 20)
b := lipgloss.RoundedBorder()
m.payloadViewport.Style = lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
payload := m.chosenMessage.Payload
var jsonPayload any
err := json.Unmarshal([]byte(payload), &jsonPayload)
if err == nil {
pretty, err := json.MarshalIndent(jsonPayload, "", " ")
if err == nil {
payload = string(pretty)
}
}
m.payloadViewport.SetContent(payload)
case 2:
chosenMessage := *m.chosenMessage
m.currentDialog = &Dialog{
Prompt: "Requeue message? It will go back to the original topic.",
Action: func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return DialogResult{
Err: m.backend.Requeue(ctx, chosenMessage),
}
},
}
case 3:
chosenMessage := *m.chosenMessage
m.currentDialog = &Dialog{
Prompt: "Acknowledge message? It will be dropped from the topic.",
Action: func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return DialogResult{
Err: m.backend.Ack(ctx, chosenMessage),
}
},
}
}
}
}
return m, nil
}
}
func (m Model) View() string {
if m.chosenMessage == nil {
return baseStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n"
}
msg := m.chosenMessage
var out string
if m.chosenMessageGone {
out += warningStyle.Render("Read only — the message is gone.")
out += "\n"
}
out += fmt.Sprintf(
"ID: %v\nUUID: %v\nOriginal Topic: %v\nDelayed For: %v\nDelayed Until: %v\nRequeue In: %v\n\n",
msg.ID,
msg.UUID,
msg.OriginalTopic,
msg.DelayedFor,
msg.DelayedUntil,
msg.RequeueIn,
)
if m.showingPayload {
out += m.payloadViewport.View()
return out
}
out += "Metadata:\n"
keys := maps.Keys(msg.Metadata)
slices.Sort(keys)
for _, k := range keys {
v := msg.Metadata[k]
out += fmt.Sprintf(" %v: %v\n", k, v)
}
if m.currentDialog != nil {
prompt := m.currentDialog.Prompt + "\n\n"
if m.currentDialog.Running {
prompt += "Running..."
} else {
for i, action := range dialogActions {
style := buttonStyle
if i == m.currentDialog.Choice {
style = buttonSelectedStyle
}
prompt += fmt.Sprintf("%v", style.MarginLeft(13).Render(action))
}
}
out += dialogStyle.Render(prompt)
} else {
out += "\nActions:\n"
messageActions := readOnlyMessageActions
if !m.chosenMessageGone {
messageActions = append(messageActions, writeMessageActions...)
}
for i, action := range messageActions {
style := buttonStyle
if i == m.chosenAction {
style = buttonSelectedStyle
}
out += fmt.Sprintf("%v\n", style.MarginLeft(2).Render(action))
}
}
return out
}
func NewModel(backend Backend) Model {
columns := []table.Column{
{Title: "ID", Width: 8},
{Title: "UUID", Width: 40},
{Title: "Original Topic", Width: 20},
{Title: "Delayed For", Width: 14},
{Title: "Requeue In", Width: 14},
}
t := table.New(
table.WithColumns(columns),
table.WithFocused(true),
table.WithHeight(20),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
t.SetStyles(s)
return Model{
backend: backend,
sub: make(chan MessagesUpdated),
table: t,
}
}
type Dialog struct {
Prompt string
Action func() tea.Msg
Choice int
Running bool
}

41
tools/pq/go.mod Normal file
View File

@@ -0,0 +1,41 @@
module github.com/ThreeDotsLabs/watermill/tools/pq
go 1.25.0
require (
github.com/ThreeDotsLabs/watermill v1.5.1
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
github.com/pkg/errors v0.9.1
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
)

88
tools/pq/go.sum Normal file
View File

@@ -0,0 +1,88 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

51
tools/pq/main.go Normal file
View File

@@ -0,0 +1,51 @@
package main
import (
"context"
"flag"
"log"
"github.com/ThreeDotsLabs/watermill/tools/pq/backend"
"github.com/ThreeDotsLabs/watermill/tools/pq/cli"
tea "github.com/charmbracelet/bubbletea"
)
var (
backendFlag = flag.String("backend", "", "backend to use")
topicFlag = flag.String("topic", "", "topic to use")
rawTopicFlag = flag.String("raw-topic", "", "raw topic to use")
)
func main() {
flag.Parse()
config := cli.BackendConfig{
Topic: *topicFlag,
RawTopic: *rawTopicFlag,
}
err := config.Validate()
if err != nil {
log.Fatal(err)
}
var b cli.Backend
switch *backendFlag {
case "postgres":
b, err = backend.NewPostgresBackend(context.Background(), config)
if err != nil {
log.Fatal(err)
}
default:
log.Fatalf("unknown backend: %s", *backendFlag)
}
m := cli.NewModel(b)
p := tea.NewProgram(m, tea.WithAltScreen())
_, err = p.Run()
if err != nil {
log.Fatal(err)
}
}