snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s
Some checks failed
master / ci (push) Failing after 0s
This commit is contained in:
38
tools/pq/README.md
Normal file
38
tools/pq/README.md
Normal 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).
|
||||
99
tools/pq/backend/postgres.go
Normal file
99
tools/pq/backend/postgres.go
Normal 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
32
tools/pq/cli/backend.go
Normal 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
45
tools/pq/cli/message.go
Normal 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
403
tools/pq/cli/model.go
Normal 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
41
tools/pq/go.mod
Normal 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
88
tools/pq/go.sum
Normal 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
51
tools/pq/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user