Skip to content

Commit 8b3685b

Browse files
committed
pg: feat: entity controller
1 parent c5f509d commit 8b3685b

File tree

6 files changed

+335
-53
lines changed

6 files changed

+335
-53
lines changed

pg/_examples/basic/main.go

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,44 @@ type Customer struct {
1212
Name string `json:"name" pg:"type=varchar(255)"`
1313
}
1414

15+
func newPostgresMiddleware() iris.Handler {
16+
schema := pg.NewSchema()
17+
schema.MustRegister("customers", Customer{})
18+
19+
opts := pg.Options{
20+
Host: "localhost",
21+
Port: 5432,
22+
User: "postgres",
23+
Password: "admin!123",
24+
DBName: "test_db",
25+
Schema: "public",
26+
SSLMode: "disable",
27+
Transactional: true, // or false to disable the transactional feature.
28+
Trace: true, // or false to production to disable query logging.
29+
CreateSchema: true, // true to create the schema if it doesn't exist.
30+
CheckSchema: true, // true to check the schema for missing tables and columns.
31+
ErrorHandler: func(ctx iris.Context, err error) {
32+
ctx.StopWithError(iris.StatusInternalServerError, err)
33+
},
34+
}
35+
36+
p := pg.New(schema, opts)
37+
// OR pg.NewFromDB(db, pg.Options{Transactional: true})
38+
return p.Handler()
39+
}
40+
1541
func main() {
1642
app := iris.New()
1743

1844
postgresMiddleware := newPostgresMiddleware()
19-
{
20-
customerAPI := app.Party("/api/customer", postgresMiddleware)
21-
customerAPI.Post("/", createCustomer)
22-
customerAPI.Get("/{id:uuid}", getCustomer)
23-
customerAPI.Put("/{id:uuid}", updateCustomer)
24-
customerAPI.Delete("/{id:uuid}", deleteCustomer)
25-
}
2645

46+
customerAPI := app.Party("/api/customer", postgresMiddleware)
47+
customerAPI.Post("/", createCustomer)
48+
customerAPI.Get("/{id:uuid}", getCustomer)
49+
customerAPI.Put("/{id:uuid}", updateCustomer)
50+
customerAPI.Delete("/{id:uuid}", deleteCustomer)
51+
52+
customerAPI.PartyConfigure("/")
2753
/*
2854
Create Customer:
2955
@@ -165,29 +191,3 @@ func deleteCustomer(ctx iris.Context) {
165191
ctx.StatusCode(iris.StatusOK)
166192
ctx.JSON(iris.Map{"message": "Customer deleted successfully"})
167193
}
168-
169-
func newPostgresMiddleware() iris.Handler {
170-
schema := pg.NewSchema()
171-
schema.MustRegister("customers", Customer{})
172-
173-
opts := pg.Options{
174-
Host: "localhost",
175-
Port: 5432,
176-
User: "postgres",
177-
Password: "admin!123",
178-
DBName: "test_db",
179-
Schema: "public",
180-
SSLMode: "disable",
181-
Transactional: true, // or false to disable the transactional feature.
182-
Trace: true, // or false to production to disable query logging.
183-
CreateSchema: true, // true to create the schema if it doesn't exist.
184-
CheckSchema: true, // true to check the schema for missing tables and columns.
185-
ErrorHandler: func(ctx iris.Context, err error) {
186-
ctx.StopWithError(iris.StatusInternalServerError, err)
187-
},
188-
}
189-
190-
p := pg.New(schema, opts)
191-
// OR pg.NewFromDB(db, pg.Options{Transactional: true})
192-
return p.Handler()
193-
}

pg/_examples/controller/main.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package main
2+
3+
import (
4+
"github.com/kataras/iris/v12"
5+
"github.com/kataras/iris/v12/x/jsonx"
6+
7+
"github.com/iris-contrib/middleware/pg"
8+
)
9+
10+
// The Customer database table model.
11+
type Customer struct {
12+
ID string `json:"id" pg:"type=uuid,primary"`
13+
CreatedAt jsonx.ISO8601 `pg:"type=timestamp,default=clock_timestamp()" json:"created_at,omitempty"`
14+
UpdatedAt jsonx.ISO8601 `pg:"type=timestamp,default=clock_timestamp()" json:"updated_at,omitempty"`
15+
16+
Name string `json:"name" pg:"type=varchar(255)"`
17+
}
18+
19+
func newPG() *pg.PG {
20+
schema := pg.NewSchema()
21+
schema.MustRegister("customers", Customer{})
22+
23+
opts := pg.Options{
24+
Host: "localhost",
25+
Port: 5432,
26+
User: "postgres",
27+
Password: "admin!123",
28+
DBName: "test_db",
29+
Schema: "public",
30+
SSLMode: "disable",
31+
Transactional: true, // or false to disable the transactional feature.
32+
Trace: true, // or false to production to disable query logging.
33+
CreateSchema: true, // true to create the schema if it doesn't exist.
34+
CheckSchema: true, // true to check the schema for missing tables and columns.
35+
ErrorHandler: func(ctx iris.Context, err error) bool {
36+
ctx.StopWithError(iris.StatusInternalServerError, err)
37+
return true
38+
},
39+
}
40+
41+
p := pg.New(schema, opts)
42+
return p
43+
}
44+
45+
func main() {
46+
app := iris.New()
47+
pgMiddleware := newPG()
48+
49+
customerController := pg.NewEntityController[Customer, string](pgMiddleware)
50+
app.PartyConfigure("/api/customer", customerController)
51+
52+
app.Listen(":8080")
53+
}

pg/entity_controller.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package pg
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/kataras/iris/v12"
9+
"github.com/kataras/iris/v12/x/errors"
10+
11+
"github.com/kataras/pg"
12+
)
13+
14+
// EntityController is a controller for a single entity.
15+
// It can be used to create a RESTful API for a single entity.
16+
// It is a wrapper around the pg.Repository.
17+
// It can be used as a base controller for a custom controller.
18+
// The T is the entity type (e.g. a custom type, Customer) and V is the ID type (e.g. string).
19+
//
20+
// The controller registers the following routes:
21+
// - POST / - creates a new entity.
22+
// - PUT / - updates an existing entity.
23+
// - GET /{id} - gets an entity by ID.
24+
// - DELETE /{id} - deletes an entity by ID.
25+
// The {id} parameter is the entity ID. It can be a string, int, uint, uuid, etc.
26+
type EntityController[T any, V comparable] struct {
27+
repository *pg.Repository[T]
28+
29+
// GetID returns the entity ID for GET/{id} and DELETE/{id} paths from the request Context.
30+
GetID func(ctx iris.Context) V
31+
32+
// ErrorHandler defaults to the PG's error handler. It can be customized for this controller.
33+
// Setting this to nil will panic the application on the first error.
34+
ErrorHandler func(ctx iris.Context, err error) bool
35+
}
36+
37+
// NewEntityController returns a new EntityController[T, V].
38+
// The T is the entity type (e.g. a custom type, Customer) and V is the ID type (e.g. string).
39+
//
40+
// Read the type's documentation for more information.
41+
func NewEntityController[T any, V comparable](middleware *PG) *EntityController[T, V] {
42+
repo := pg.NewRepository[T](middleware.GetDB())
43+
errorHandler := middleware.opts.handleError
44+
45+
return &EntityController[T, V]{
46+
repository: repo,
47+
ErrorHandler: errorHandler,
48+
}
49+
}
50+
51+
// Configure registers the controller's routes.
52+
// It is called automatically by the Iris API Builder when registered to the Iris Application.
53+
func (c *EntityController[T, V]) Configure(r iris.Party) {
54+
var v V
55+
typ := reflect.TypeOf(v)
56+
idParamName := fmt.Sprintf("%s_id", strings.ToLower(typ.Name()))
57+
idParam := fmt.Sprintf("/{%s}", idParamName)
58+
switch typ.Kind() {
59+
case reflect.String:
60+
idParam = fmt.Sprintf("/{%s:uuid}", idParamName)
61+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
62+
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
63+
idParam = fmt.Sprintf("/{%s:int}", idParamName)
64+
}
65+
66+
if c.GetID == nil {
67+
c.GetID = func(ctx iris.Context) V {
68+
return ctx.Params().GetEntry(idParamName).Value().(V)
69+
}
70+
}
71+
72+
r.Post("/", c.create)
73+
r.Put("/", c.update)
74+
r.Get(idParam, c.get)
75+
r.Delete(idParam, c.delete)
76+
}
77+
78+
// handleError handles the error. It returns true if the error was handled.
79+
func (c *EntityController[T, V]) handleError(ctx iris.Context, err error) bool {
80+
if err == nil {
81+
return false
82+
}
83+
84+
return c.ErrorHandler(ctx, err)
85+
}
86+
87+
// readPayload reads the request body and returns the entity.
88+
func (c *EntityController[T, V]) readPayload(ctx iris.Context) (T, bool) {
89+
var payload T
90+
err := ctx.ReadJSON(&payload)
91+
if err != nil {
92+
errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error())
93+
return payload, false
94+
}
95+
96+
return payload, true
97+
}
98+
99+
type idPayload[T comparable] struct {
100+
ID T `json:"id"`
101+
}
102+
103+
// create creates a new entity.
104+
func (c *EntityController[T, V]) create(ctx iris.Context) {
105+
entry, ok := c.readPayload(ctx)
106+
if !ok {
107+
return
108+
}
109+
110+
var id V
111+
err := c.repository.InsertSingle(ctx, entry, &id)
112+
if c.handleError(ctx, err) {
113+
return
114+
}
115+
116+
ctx.StatusCode(iris.StatusCreated)
117+
ctx.JSON(idPayload[V]{ID: id})
118+
}
119+
120+
// get gets an entity by ID.
121+
func (c *EntityController[T, V]) get(ctx iris.Context) {
122+
id := c.GetID(ctx)
123+
124+
entry, err := c.repository.SelectByID(ctx, id)
125+
if c.handleError(ctx, err) {
126+
return
127+
}
128+
129+
ctx.JSON(entry)
130+
}
131+
132+
// update updates an entity.
133+
func (c *EntityController[T, V]) update(ctx iris.Context) {
134+
entry, ok := c.readPayload(ctx)
135+
if !ok {
136+
return
137+
}
138+
139+
// patch-like.
140+
onlyColumns := ctx.URLParamSlice("columns")
141+
142+
var (
143+
n int64
144+
err error
145+
)
146+
if len(onlyColumns) > 0 {
147+
n, err = c.repository.UpdateOnlyColumns(ctx, onlyColumns, entry)
148+
} else {
149+
// put-like.
150+
n, err = c.repository.Update(ctx, entry)
151+
}
152+
if c.handleError(ctx, err) {
153+
return
154+
}
155+
156+
if n == 0 {
157+
errors.NotFound.Message(ctx, "resource not found")
158+
return
159+
}
160+
161+
ctx.StatusCode(iris.StatusNoContent)
162+
// ctx.StatusCode(iris.StatusOK)
163+
// ctx.JSON(iris.Map{"message": "resource updated successfully"})
164+
}
165+
166+
// delete deletes an entity by ID.
167+
func (c *EntityController[T, V]) delete(ctx iris.Context) {
168+
id := c.GetID(ctx)
169+
170+
ok, err := c.repository.DeleteByID(ctx, id)
171+
if c.handleError(ctx, err) {
172+
return
173+
}
174+
175+
if !ok {
176+
errors.NotFound.Details(ctx, "resource not found", err.Error())
177+
return
178+
}
179+
180+
ctx.StatusCode(iris.StatusNoContent)
181+
}

pg/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.21
55
require (
66
github.com/kataras/golog v0.1.9
77
github.com/kataras/iris/v12 v12.2.4
8-
github.com/kataras/pg v1.0.3
8+
github.com/kataras/pg v1.0.4
99
github.com/kataras/pgx-golog v0.0.1
1010
)
1111

@@ -50,7 +50,7 @@ require (
5050
github.com/yosssi/ace v0.0.5 // indirect
5151
golang.org/x/crypto v0.12.0 // indirect
5252
golang.org/x/net v0.14.0 // indirect
53-
golang.org/x/sync v0.1.0 // indirect
53+
golang.org/x/sync v0.3.0 // indirect
5454
golang.org/x/sys v0.11.0 // indirect
5555
golang.org/x/text v0.12.0 // indirect
5656
golang.org/x/time v0.3.0 // indirect

pg/go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
6262
github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY=
6363
github.com/kataras/iris/v12 v12.2.4 h1:fj5y2usjhnzTPrPsL/94wGaCcirvR/EdshgQgx2lBCo=
6464
github.com/kataras/iris/v12 v12.2.4/go.mod h1:4zzcsafozAKy9SUwSZ7Qx1TVY8NZJVZXk5mgDeksXec=
65-
github.com/kataras/pg v1.0.3 h1:q1Q0y0VA9wbZrH5iIm5akbup4YfL69YG0Yw4mpprjoY=
66-
github.com/kataras/pg v1.0.3/go.mod h1:cJhxppDwpjymEnhebZ+56tgWYLgpdZutM6O5QeEjby4=
65+
github.com/kataras/pg v1.0.4 h1:IqM+6EStIdTrugD5ekZ/eqfB3mZxCXcTkQAd3LHuKIY=
66+
github.com/kataras/pg v1.0.4/go.mod h1:BLSUSkLSf+/zJ+mFffB/YQUUWbH4TSCqZTlAFHpHLLA=
6767
github.com/kataras/pgx-golog v0.0.1 h1:e8bankbEM/2rKLgtb6wiiB0ze5nY+6cx3wmr1bj+KEI=
6868
github.com/kataras/pgx-golog v0.0.1/go.mod h1:lnfwUCGl9cPXNwu1yiepE+aal6N1vJmpCgb+UGy1p7k=
6969
github.com/kataras/pio v0.0.12 h1:o52SfVYauS3J5X08fNjlGS5arXHjW/ItLkyLcKjoH6w=
@@ -149,8 +149,8 @@ golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
149149
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
150150
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
151151
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
152-
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
153-
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
152+
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
153+
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
154154
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
155155
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
156156
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

0 commit comments

Comments
 (0)