Skip to content

Commit 47af12b

Browse files
committed
Move API framework to its own repo for reuse
Here, move the API framework, which is currently duplicated in two projects, over to its own repository so that we can reuse it between them. It's new home will be `riverapiframe` [1]. [1] riverqueue/apiframe#1
1 parent 526b574 commit 47af12b

14 files changed

Lines changed: 50 additions & 1209 deletions

cmd/riverui/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import (
1818

1919
"github.com/riverqueue/river"
2020
"github.com/riverqueue/river/riverdriver/riverpgxv5"
21+
"github.com/riverqueue/riverapiframe/apimiddleware"
2122

2223
"riverqueue.com/riverui"
23-
"riverqueue.com/riverui/internal/apimiddleware"
2424
)
2525

2626
func main() {

go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
module riverqueue.com/riverui
22

3-
go 1.22.0
3+
go 1.23.0
44

5-
toolchain go1.23.5
5+
toolchain go1.24.1
66

77
require (
8-
github.com/go-playground/validator/v10 v10.25.0
98
github.com/google/uuid v1.6.0
10-
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
119
github.com/jackc/pgx/v5 v5.7.2
1210
github.com/riverqueue/river v0.18.0
1311
github.com/riverqueue/river/riverdriver v0.18.0
1412
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.18.0
1513
github.com/riverqueue/river/rivershared v0.18.0
1614
github.com/riverqueue/river/rivertype v0.18.0
15+
github.com/riverqueue/riverapiframe v0.0.0-20250309175631-906b5d376c73
1716
github.com/rs/cors v1.11.1
1817
github.com/samber/slog-http v1.5.1
1918
github.com/stretchr/testify v1.10.0
@@ -24,6 +23,8 @@ require (
2423
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
2524
github.com/go-playground/locales v0.14.1 // indirect
2625
github.com/go-playground/universal-translator v0.18.1 // indirect
26+
github.com/go-playground/validator/v10 v10.25.0 // indirect
27+
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
2728
github.com/jackc/pgpassfile v1.0.0 // indirect
2829
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
2930
github.com/jackc/puddle/v2 v2.2.2 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ github.com/riverqueue/river/rivershared v0.18.0 h1:hBfyaoTAvogs7lSw4vr6A2ZdZmmtT
4747
github.com/riverqueue/river/rivershared v0.18.0/go.mod h1:wyJw90ILEYNcYCoXr4B6iPHnSyRH0WKGQuPzjdEwou8=
4848
github.com/riverqueue/river/rivertype v0.18.0 h1:YsXR5NbLAzniurGO0+zcISWMKq7Y71xkIe2oi86OAsE=
4949
github.com/riverqueue/river/rivertype v0.18.0/go.mod h1:DETcejveWlq6bAb8tHkbgJqmXWVLiFhTiEm8j7co1bE=
50+
github.com/riverqueue/riverapiframe v0.0.0-20250309175631-906b5d376c73 h1:qh7TNX7IjuuEfRA35/OS0pQn6bh8uV2EkV0qp6rV4nA=
51+
github.com/riverqueue/riverapiframe v0.0.0-20250309175631-906b5d376c73/go.mod h1:I65DmgxIYNnjXigyhfWsAkGYEcFQlvduWoybAPbwLIA=
5052
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
5153
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
52-
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
53-
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
54+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
55+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
5456
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
5557
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
5658
github.com/samber/slog-http v1.5.1 h1:z5Ty/u5LKJbWjmjLDr6OgtwHXrPPrH2ZPoQE/47n9sU=

handler.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ import (
2121
"github.com/riverqueue/river/rivershared/baseservice"
2222
"github.com/riverqueue/river/rivershared/startstop"
2323
"github.com/riverqueue/river/rivershared/util/valutil"
24-
25-
"riverqueue.com/riverui/internal/apiendpoint"
26-
"riverqueue.com/riverui/internal/apimiddleware"
24+
"github.com/riverqueue/riverapiframe/apiendpoint"
25+
"github.com/riverqueue/riverapiframe/apimiddleware"
2726
)
2827

2928
// DB is the interface for a pgx database connection.

handler_api_endpoint.go

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import (
1919
"github.com/riverqueue/river/rivershared/util/ptrutil"
2020
"github.com/riverqueue/river/rivershared/util/sliceutil"
2121
"github.com/riverqueue/river/rivertype"
22+
"github.com/riverqueue/riverapiframe/apiendpoint"
23+
"github.com/riverqueue/riverapiframe/apierror"
2224

23-
"riverqueue.com/riverui/internal/apiendpoint"
24-
"riverqueue.com/riverui/internal/apierror"
2525
"riverqueue.com/riverui/internal/dbsqlc"
2626
"riverqueue.com/riverui/internal/querycacher"
2727
"riverqueue.com/riverui/internal/util/pgxutil"
@@ -104,7 +104,7 @@ func (a *healthCheckGetEndpoint) Execute(ctx context.Context, req *healthCheckGe
104104
// fall through to OK status response below
105105

106106
default:
107-
return nil, apierror.NewNotFound("Health check %q not found. Use either `complete` or `minimal`.", req.Name)
107+
return nil, apierror.NewNotFoundf("Health check %q not found. Use either `complete` or `minimal`.", req.Name)
108108
}
109109

110110
return statusResponseOK, nil
@@ -142,7 +142,7 @@ func (a *jobCancelEndpoint) Execute(ctx context.Context, req *jobCancelRequest)
142142
job, err := a.client.JobCancelTx(ctx, tx, jobID)
143143
if err != nil {
144144
if errors.Is(err, river.ErrNotFound) {
145-
return nil, apierror.NewNotFoundJob(jobID)
145+
return nil, NewNotFoundJob(jobID)
146146
}
147147
return nil, err
148148
}
@@ -185,10 +185,10 @@ func (a *jobDeleteEndpoint) Execute(ctx context.Context, req *jobDeleteRequest)
185185
_, err := a.client.JobDeleteTx(ctx, tx, jobID)
186186
if err != nil {
187187
if errors.Is(err, rivertype.ErrJobRunning) {
188-
return nil, apierror.NewBadRequest("Job %d is running and can't be deleted until it finishes.", jobID)
188+
return nil, apierror.NewBadRequestf("Job %d is running and can't be deleted until it finishes.", jobID)
189189
}
190190
if errors.Is(err, river.ErrNotFound) {
191-
return nil, apierror.NewNotFoundJob(jobID)
191+
return nil, NewNotFoundJob(jobID)
192192
}
193193
return nil, err
194194
}
@@ -227,7 +227,7 @@ func (req *jobGetRequest) ExtractRaw(r *http.Request) error {
227227

228228
jobID, err := strconv.ParseInt(idString, 10, 64)
229229
if err != nil {
230-
return apierror.NewBadRequest("Couldn't convert job ID to int64: %s.", err)
230+
return apierror.NewBadRequestf("Couldn't convert job ID to int64: %s.", err)
231231
}
232232
req.JobID = jobID
233233

@@ -239,7 +239,7 @@ func (a *jobGetEndpoint) Execute(ctx context.Context, req *jobGetRequest) (*Rive
239239
job, err := a.client.JobGetTx(ctx, tx, req.JobID)
240240
if err != nil {
241241
if errors.Is(err, river.ErrNotFound) {
242-
return nil, apierror.NewNotFoundJob(req.JobID)
242+
return nil, NewNotFoundJob(req.JobID)
243243
}
244244
return nil, fmt.Errorf("error getting job: %w", err)
245245
}
@@ -276,7 +276,7 @@ func (req *jobListRequest) ExtractRaw(r *http.Request) error {
276276
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
277277
limit, err := strconv.Atoi(limitStr)
278278
if err != nil {
279-
return apierror.NewBadRequest("Couldn't convert `limit` to integer: %s.", err)
279+
return apierror.NewBadRequestf("Couldn't convert `limit` to integer: %s.", err)
280280
}
281281

282282
req.Limit = &limit
@@ -344,7 +344,7 @@ func (a *jobRetryEndpoint) Execute(ctx context.Context, req *jobRetryRequest) (*
344344
_, err := a.client.JobRetryTx(ctx, tx, jobID)
345345
if err != nil {
346346
if errors.Is(err, river.ErrNotFound) {
347-
return nil, apierror.NewNotFoundJob(jobID)
347+
return nil, NewNotFoundJob(jobID)
348348
}
349349
return nil, err
350350
}
@@ -388,7 +388,7 @@ func (a *queueGetEndpoint) Execute(ctx context.Context, req *queueGetRequest) (*
388388
queue, err := a.client.QueueGetTx(ctx, tx, req.Name)
389389
if err != nil {
390390
if errors.Is(err, river.ErrNotFound) {
391-
return nil, apierror.NewNotFoundQueue(req.Name)
391+
return nil, NewNotFoundQueue(req.Name)
392392
}
393393
return nil, fmt.Errorf("error getting queue: %w", err)
394394
}
@@ -430,7 +430,7 @@ func (req *queueListRequest) ExtractRaw(r *http.Request) error {
430430
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
431431
limit, err := strconv.Atoi(limitStr)
432432
if err != nil {
433-
return apierror.NewBadRequest("Couldn't convert `limit` to integer: %s.", err)
433+
return apierror.NewBadRequestf("Couldn't convert `limit` to integer: %s.", err)
434434
}
435435

436436
req.Limit = &limit
@@ -490,7 +490,7 @@ func (a *queuePauseEndpoint) Execute(ctx context.Context, req *queuePauseRequest
490490
return pgxutil.WithTxV(ctx, a.dbPool, func(ctx context.Context, tx pgx.Tx) (*statusResponse, error) {
491491
if err := a.client.QueuePauseTx(ctx, tx, req.Name, nil); err != nil {
492492
if errors.Is(err, river.ErrNotFound) {
493-
return nil, apierror.NewNotFoundQueue(req.Name)
493+
return nil, NewNotFoundQueue(req.Name)
494494
}
495495
return nil, fmt.Errorf("error pausing queue: %w", err)
496496
}
@@ -532,7 +532,7 @@ func (a *queueResumeEndpoint) Execute(ctx context.Context, req *queueResumeReque
532532
return pgxutil.WithTxV(ctx, a.dbPool, func(ctx context.Context, tx pgx.Tx) (*statusResponse, error) {
533533
if err := a.client.QueueResumeTx(ctx, tx, req.Name, nil); err != nil {
534534
if errors.Is(err, river.ErrNotFound) {
535-
return nil, apierror.NewNotFoundQueue(req.Name)
535+
return nil, NewNotFoundQueue(req.Name)
536536
}
537537
return nil, fmt.Errorf("error resuming queue: %w", err)
538538
}
@@ -670,7 +670,7 @@ func (a *workflowGetEndpoint) Execute(ctx context.Context, req *workflowGetReque
670670
}
671671

672672
if len(jobs) < 1 {
673-
return nil, apierror.NewNotFoundWorkflow(req.ID)
673+
return nil, NewNotFoundWorkflow(req.ID)
674674
}
675675

676676
return &workflowGetResponse{
@@ -712,7 +712,7 @@ func (req *workflowListRequest) ExtractRaw(r *http.Request) error {
712712
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
713713
limit, err := strconv.Atoi(limitStr)
714714
if err != nil {
715-
return apierror.NewBadRequest("Couldn't convert `limit` to integer: %s.", err)
715+
return apierror.NewBadRequestf("Couldn't convert `limit` to integer: %s.", err)
716716
}
717717

718718
req.Limit = &limit
@@ -758,6 +758,18 @@ func (a *workflowListEndpoint) Execute(ctx context.Context, req *workflowListReq
758758
}
759759
}
760760

761+
func NewNotFoundJob(jobID int64) *apierror.NotFound {
762+
return apierror.NewNotFoundf("Job not found: %d.", jobID)
763+
}
764+
765+
func NewNotFoundQueue(name string) *apierror.NotFound {
766+
return apierror.NewNotFoundf("Queue not found: %s.", name)
767+
}
768+
769+
func NewNotFoundWorkflow(id string) *apierror.NotFound {
770+
return apierror.NewNotFoundf("Workflow not found: %s.", id)
771+
}
772+
761773
type RiverJob struct {
762774
ID int64 `json:"id"`
763775
Args json.RawMessage `json:"args"`

handler_api_endpoint_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616
"github.com/riverqueue/river/rivershared/startstop"
1717
"github.com/riverqueue/river/rivershared/util/ptrutil"
1818
"github.com/riverqueue/river/rivertype"
19+
"github.com/riverqueue/riverapiframe/apierror"
1920

20-
"riverqueue.com/riverui/internal/apierror"
2121
"riverqueue.com/riverui/internal/riverinternaltest"
2222
"riverqueue.com/riverui/internal/riverinternaltest/testfactory"
2323
)
@@ -104,7 +104,7 @@ func TestHandlerHealthCheckGetEndpoint(t *testing.T) {
104104
endpoint, _ := setupEndpoint(ctx, t, newHealthCheckGetEndpoint)
105105

106106
_, err := endpoint.Execute(ctx, &healthCheckGetRequest{Name: "other"})
107-
requireAPIError(t, apierror.NewNotFound("Health check %q not found. Use either `complete` or `minimal`.", "other"), err)
107+
requireAPIError(t, apierror.NewNotFoundf("Health check %q not found. Use either `complete` or `minimal`.", "other"), err)
108108
})
109109
}
110110

@@ -140,7 +140,7 @@ func TestJobCancelEndpoint(t *testing.T) {
140140
endpoint, _ := setupEndpoint(ctx, t, newJobCancelEndpoint)
141141

142142
_, err := endpoint.Execute(ctx, &jobCancelRequest{JobIDs: []int64String{123}})
143-
requireAPIError(t, apierror.NewNotFoundJob(123), err)
143+
requireAPIError(t, NewNotFoundJob(123), err)
144144
})
145145
}
146146

@@ -174,7 +174,7 @@ func TestJobDeleteEndpoint(t *testing.T) {
174174
endpoint, _ := setupEndpoint(ctx, t, newJobDeleteEndpoint)
175175

176176
_, err := endpoint.Execute(ctx, &jobDeleteRequest{JobIDs: []int64String{123}})
177-
requireAPIError(t, apierror.NewNotFoundJob(123), err)
177+
requireAPIError(t, NewNotFoundJob(123), err)
178178
})
179179
}
180180

@@ -201,7 +201,7 @@ func TestJobGetEndpoint(t *testing.T) {
201201
endpoint, _ := setupEndpoint(ctx, t, newJobGetEndpoint)
202202

203203
_, err := endpoint.Execute(ctx, &jobGetRequest{JobID: 123})
204-
requireAPIError(t, apierror.NewNotFoundJob(123), err)
204+
requireAPIError(t, NewNotFoundJob(123), err)
205205
})
206206
}
207207

@@ -322,7 +322,7 @@ func TestJobRetryEndpoint(t *testing.T) {
322322
endpoint, _ := setupEndpoint(ctx, t, newJobRetryEndpoint)
323323

324324
_, err := endpoint.Execute(ctx, &jobRetryRequest{JobIDs: []int64String{123}})
325-
requireAPIError(t, apierror.NewNotFoundJob(123), err)
325+
requireAPIError(t, NewNotFoundJob(123), err)
326326
})
327327
}
328328

@@ -353,7 +353,7 @@ func TestAPIHandlerQueueGet(t *testing.T) {
353353
endpoint, _ := setupEndpoint(ctx, t, newQueueGetEndpoint)
354354

355355
_, err := endpoint.Execute(ctx, &queueGetRequest{Name: "does_not_exist"})
356-
requireAPIError(t, apierror.NewNotFoundQueue("does_not_exist"), err)
356+
requireAPIError(t, NewNotFoundQueue("does_not_exist"), err)
357357
})
358358
}
359359

@@ -420,7 +420,7 @@ func TestAPIHandlerQueuePause(t *testing.T) {
420420
endpoint, _ := setupEndpoint(ctx, t, newQueuePauseEndpoint)
421421

422422
_, err := endpoint.Execute(ctx, &queuePauseRequest{Name: "does_not_exist"})
423-
requireAPIError(t, apierror.NewNotFoundQueue("does_not_exist"), err)
423+
requireAPIError(t, NewNotFoundQueue("does_not_exist"), err)
424424
})
425425
}
426426

@@ -449,7 +449,7 @@ func TestAPIHandlerQueueResume(t *testing.T) {
449449
endpoint, _ := setupEndpoint(ctx, t, newQueueResumeEndpoint)
450450

451451
_, err := endpoint.Execute(ctx, &queueResumeRequest{Name: "does_not_exist"})
452-
requireAPIError(t, apierror.NewNotFoundQueue("does_not_exist"), err)
452+
requireAPIError(t, NewNotFoundQueue("does_not_exist"), err)
453453
})
454454
}
455455

@@ -577,7 +577,7 @@ func TestAPIHandlerWorkflowGet(t *testing.T) {
577577
workflowID := uuid.New()
578578

579579
_, err := endpoint.Execute(ctx, &workflowGetRequest{ID: workflowID.String()})
580-
requireAPIError(t, apierror.NewNotFoundWorkflow(workflowID.String()), err)
580+
requireAPIError(t, NewNotFoundWorkflow(workflowID.String()), err)
581581
})
582582
}
583583

0 commit comments

Comments
 (0)