Skip to content

Commit b06c47d

Browse files
committed
fix create
1 parent 405d097 commit b06c47d

File tree

11 files changed

+167
-63
lines changed

11 files changed

+167
-63
lines changed

README.md

+13-6
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ Examples of using various popular database libraries and ORM in Go.
1111
The aim is to demonstrate and compare usage for several operations
1212

1313
1. Simple CRUD operation
14-
2. 1-1 queries
15-
3. 1-to-Many queries
16-
4. Many-to-many queries
17-
5. Dynamic list filter from query parameter
18-
6. Transaction
14+
2. 1-to-Many queries
15+
3. Many-to-many queries
16+
4. Dynamic list filter from query parameter
17+
5. Transaction
1918

20-
The schema contains optional fields, for example middle name, and a field that must not be returned, for example, a password.
19+
# Schema
20+
21+
![Database](db.png)
22+
23+
There are four tables. The `users` table and `addresses` table are linked by the pivot `user_addresses` table. The `addresses` table contains a foreign key to `countries` to demonstrate 1-to-many relationship.
24+
25+
To make things interesting, we make `middle_name` an optional field. We also have a 'protected/hidden' field in which we do not want to return in a JSON response, like a password.
2126

2227

2328
# Usage
@@ -30,6 +35,8 @@ Setup postgres database by either running from docker-compose or manually.
3035

3136
This creates both `postgres` database (which this repo uses) and `ent` database which is used by ent ORM.
3237

38+
If you create the database manually, execute the `database/01-schema.sql` script.
39+
3340
Default database credentials are defined in `config/config.go`. These can be overwritten by setting environment variables. For example:
3441

3542
export DB_NAME=test_db

db.png

87.4 KB
Loading

db/ent/ent/schema/user.go

+73
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package schema
22

33
import (
4+
"context"
45
"entgo.io/ent"
56
"entgo.io/ent/schema/edge"
67
"entgo.io/ent/schema/field"
8+
"entgo.io/ent/schema/mixin"
9+
"fmt"
10+
"time"
711
)
812

913
// User holds the schema definition for the User entity.
@@ -36,3 +40,72 @@ func (User) Edges() []ent.Edge {
3640
edge.From("addresses", Address.Type).Ref("users"),
3741
}
3842
}
43+
44+
// AuditMixin implements the ent.Mixin for sharing
45+
// audit-log capabilities with package schemas.
46+
type AuditMixin struct {
47+
mixin.Schema
48+
}
49+
50+
// Fields of the AuditMixin.
51+
func (AuditMixin) Fields() []ent.Field {
52+
return []ent.Field{
53+
field.Time("created_at").
54+
Immutable().
55+
Default(time.Now),
56+
field.Int("created_by").
57+
Optional(),
58+
field.Time("updated_at").
59+
Default(time.Now).
60+
UpdateDefault(time.Now),
61+
field.Int("updated_by").
62+
Optional(),
63+
}
64+
}
65+
66+
// Hooks of the AuditMixin.
67+
func (AuditMixin) Hooks() []ent.Hook {
68+
return []ent.Hook{
69+
AuditHook,
70+
}
71+
}
72+
73+
// A AuditHook is an example for audit-log hook.
74+
func AuditHook(next ent.Mutator) ent.Mutator {
75+
// AuditLogger wraps the methods that are shared between all mutations of
76+
// schemas that embed the AuditLog mixin. The variable "exists" is true, if
77+
// the field already exists in the mutation (e.g. was set by a different hook).
78+
type AuditLogger interface {
79+
SetCreatedAt(time.Time)
80+
CreatedAt() (value time.Time, exists bool)
81+
SetCreatedBy(int)
82+
CreatedBy() (id int, exists bool)
83+
SetUpdatedAt(time.Time)
84+
UpdatedAt() (value time.Time, exists bool)
85+
SetUpdatedBy(int)
86+
UpdatedBy() (id int, exists bool)
87+
}
88+
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
89+
ml, ok := m.(AuditLogger)
90+
if !ok {
91+
return nil, fmt.Errorf("unexpected audit-log call from mutation type %T", m)
92+
}
93+
usr := ctx.Value("user_id").(int)
94+
//if err != nil {
95+
// return nil, err
96+
//}
97+
switch op := m.Op(); {
98+
case op.Is(ent.OpCreate):
99+
ml.SetCreatedAt(time.Now())
100+
if _, exists := ml.CreatedBy(); !exists {
101+
ml.SetCreatedBy(usr)
102+
}
103+
case op.Is(ent.OpUpdateOne | ent.OpUpdate):
104+
ml.SetUpdatedAt(time.Now())
105+
if _, exists := ml.UpdatedBy(); !exists {
106+
ml.SetUpdatedBy(usr)
107+
}
108+
}
109+
return next.Mutate(ctx, m)
110+
})
111+
}

db/gorm/simple.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func NewRepo(db *gorm.DB) *repo {
1818
}
1919
}
2020

21-
func (r *repo) Create(ctx context.Context, u *sqlx.UserRequest, hash string) {
21+
func (r *repo) Create(ctx context.Context, u *sqlx.UserRequest, hash string) *User {
2222
user := &User{
2323
FirstName: u.FirstName,
2424
MiddleName: sql.NullString{
@@ -31,6 +31,8 @@ func (r *repo) Create(ctx context.Context, u *sqlx.UserRequest, hash string) {
3131
}
3232

3333
r.db.WithContext(ctx).Create(user)
34+
35+
return user
3436
}
3537

3638
func (r *repo) List(ctx context.Context) ([]*sqlx.UserResponse, error) {

db/sqlx/handler.go

+15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package sqlx
22

33
import (
44
"encoding/json"
5+
"errors"
6+
"github.com/jackc/pgconn"
7+
"github.com/jackc/pgerrcode"
58
"net/http"
69

710
"github.com/alexedwards/argon2id"
@@ -52,6 +55,18 @@ func (h *handler) Create(w http.ResponseWriter, r *http.Request) {
5255

5356
_, err = h.db.Create(r.Context(), &request, hash)
5457
if err != nil {
58+
var pgErr *pgconn.PgError
59+
if errors.As(err, &pgErr) {
60+
switch pgErr.Code {
61+
case pgerrcode.UniqueViolation:
62+
http.Error(w, `{"message": "`+ErrUniqueKeyViolation.Error()+`"}`, http.StatusBadRequest)
63+
return
64+
default:
65+
http.Error(w, `{"message": "`+ErrDefault.Error()+`"}`, http.StatusBadRequest)
66+
return
67+
}
68+
}
69+
5570
http.Error(w, `{"message": "`+err.Error()+`"}`, http.StatusBadRequest)
5671
return
5772
}

db/sqlx/manyToMany.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (r *database) ListM2M(ctx context.Context) ([]*UserResponseWithAddressesSql
4747

4848
var all []*UserResponseWithAddressesSqlx
4949
for users.Next() {
50-
var u user
50+
var u userDB
5151
if err := users.Scan(&u.ID, &u.FirstName, &u.MiddleName, &u.LastName, &u.Email); err != nil {
5252
return nil, fmt.Errorf("db scanning error")
5353
}

db/sqlx/requestResponse.go

+22-22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sqlx
22

33
import (
44
"database/sql"
5+
56
"godb/db/ent/ent/gen"
67
)
78

@@ -23,39 +24,38 @@ type UserUpdateRequest struct {
2324
}
2425

2526
type UserResponse struct {
26-
ID uint `json:"id,omitempty" db:"id"`
27-
FirstName string `json:"first_name" db:"first_name"`
28-
MiddleName string `json:"middle_name,omitempty" db:"middle_name"`
29-
LastName string `json:"last_name" db:"last_name"`
30-
Email string `json:"email" db:"email"`
27+
ID uint `json:"id,omitempty"`
28+
FirstName string `json:"first_name"`
29+
MiddleName string `json:"middle_name,omitempty"`
30+
LastName string `json:"last_name"`
31+
Email string `json:"email"`
3132
}
3233

3334
type UserResponseWithAddress struct {
34-
ID uint `json:"id,omitempty" db:"id"`
35-
FirstName string `json:"first_name" db:"first_name"`
36-
MiddleName string `json:"middle_name,omitempty" db:"middle_name"`
37-
LastName string `json:"last_name" db:"last_name"`
38-
Email string `json:"email" db:"email"`
35+
ID uint `json:"id,omitempty"`
36+
FirstName string `json:"first_name"`
37+
MiddleName string `json:"middle_name,omitempty"`
38+
LastName string `json:"last_name"`
39+
Email string `json:"email"`
3940
Address AddressForCountry `json:"address"`
4041
}
4142

4243
type UserResponseWithAddressesSqlx struct {
43-
ID uint `json:"id,omitempty" db:"id"`
44-
FirstName string `json:"first_name" db:"first_name"`
45-
MiddleName string `json:"middle_name,omitempty" db:"middle_name"`
46-
LastName string `json:"last_name" db:"last_name"`
47-
Email string `json:"email" db:"email"`
44+
ID uint `json:"id,omitempty"`
45+
FirstName string `json:"first_name"`
46+
MiddleName string `json:"middle_name,omitempty"`
47+
LastName string `json:"last_name"`
48+
Email string `json:"email"`
4849
Address []AddressForCountry `json:"address"`
4950
}
5051

5152
type UserResponseWithAddresses struct {
52-
ID uint `json:"id,omitempty" `
53-
FirstName string `json:"first_name"`
54-
MiddleName *string `json:"middle_name,omitempty"`
55-
//MiddleName null.String `json:"middle_name,omitempty"`
56-
LastName string `json:"last_name"`
57-
Email string `json:"email"`
58-
Address []*gen.Address `json:"address"`
53+
ID uint `json:"id,omitempty" `
54+
FirstName string `json:"first_name"`
55+
MiddleName *string `json:"middle_name,omitempty"`
56+
LastName string `json:"last_name"`
57+
Email string `json:"email"`
58+
Address []*gen.Address `json:"address"`
5959
}
6060

6161
type AddressResponse struct {

db/sqlx/simple.go

+26-31
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ package sqlx
33
import (
44
"context"
55
"database/sql"
6-
"errors"
76
"fmt"
87

9-
"github.com/jackc/pgconn"
10-
"github.com/jackc/pgerrcode"
118
"github.com/jmoiron/sqlx"
129
)
1310

1411
const (
15-
Insert = "INSERT INTO users (first_name, middle_name, last_name, email, password) VALUES ($1, $2, $3, $4, $5)"
12+
Insert = "INSERT INTO users (first_name, middle_name, last_name, email, password) VALUES ($1, $2, $3, $4, $5) RETURNING id, first_name, middle_name, last_name, email, password"
1613
List = "SELECT * FROM users;"
1714
Get = "SELECT * FROM users WHERE id = $1;"
1815
Update = "UPDATE users set first_name=$1, middle_name=$2, last_name=$3, email=$4 WHERE id=$5;"
@@ -34,39 +31,37 @@ func NewRepo(db *sqlx.DB) *database {
3431
}
3532
}
3633

37-
func (r *database) Create(ctx context.Context, request *UserRequest, hash string) (sql.Result, error) {
38-
_, err := r.db.ExecContext(ctx, Insert,
34+
func (r *database) Create(ctx context.Context, request *UserRequest, hash string) (*userDB, error) {
35+
var u userDB
36+
err := r.db.QueryRowContext(ctx, Insert,
3937
request.FirstName,
4038
request.MiddleName,
4139
request.LastName,
4240
request.Email,
4341
hash,
42+
).Scan(
43+
&u.ID,
44+
&u.FirstName,
45+
&u.MiddleName,
46+
&u.LastName,
47+
&u.Email,
48+
&u.Password,
4449
)
4550
if err != nil {
46-
var pgErr *pgconn.PgError
47-
if errors.As(err, &pgErr) {
48-
switch pgErr.Code {
49-
case pgerrcode.UniqueViolation:
50-
return nil, ErrUniqueKeyViolation
51-
default:
52-
return nil, ErrDefault
53-
}
54-
} else {
55-
return nil, ErrDefault
56-
}
51+
return nil, fmt.Errorf("error creating user record: %w", err)
5752
}
5853

59-
return nil, nil
54+
return &u, nil
6055
}
6156

6257
func (r *database) List(ctx context.Context) (users []*UserResponse, err error) {
6358
rows, err := r.db.QueryContext(ctx, List)
6459
if err != nil {
65-
return nil, fmt.Errorf("db error")
60+
return nil, fmt.Errorf("error retrieving user records")
6661
}
6762

6863
for rows.Next() {
69-
var u user
64+
var u userDB
7065
err := rows.Scan(&u.ID, &u.FirstName, &u.MiddleName, &u.LastName, &u.Email, &u.Password)
7166
if err != nil {
7267
return nil, fmt.Errorf("db scanning error")
@@ -82,18 +77,9 @@ func (r *database) List(ctx context.Context) (users []*UserResponse, err error)
8277
return users, nil
8378
}
8479

85-
type user struct {
86-
ID uint `db:"id"`
87-
FirstName string `db:"first_name"`
88-
MiddleName sql.NullString `db:"middle_name"`
89-
LastName string `db:"last_name"`
90-
Email string `db:"email"`
91-
Password string `db:"password"`
92-
}
93-
9480
func (r *database) Get(ctx context.Context, userID int64) (*UserResponse, error) {
95-
var u user
96-
err := r.db.Get(&u, Get, userID)
81+
var u userDB
82+
err := r.db.GetContext(ctx, &u, Get, userID)
9783
if err != nil {
9884
return nil, fmt.Errorf("db error")
9985
}
@@ -120,3 +106,12 @@ func (r *database) Update(ctx context.Context, userID int64, req *UserUpdateRequ
120106
func (r *database) Delete(ctx context.Context, userID int64) (sql.Result, error) {
121107
return r.db.ExecContext(ctx, Delete, userID)
122108
}
109+
110+
type userDB struct {
111+
ID uint `db:"id"`
112+
FirstName string `db:"first_name"`
113+
MiddleName sql.NullString `db:"middle_name"`
114+
LastName string `db:"last_name"`
115+
Email string `db:"email"`
116+
Password string `db:"password"`
117+
}

example/rest.http

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Content-Type: application/json
55
{
66
"first_name": "Jake",
77
"last_name": "Doe",
8-
"email": "jake2@example.com",
8+
"email": "jake26@example.com",
99
"password": "password"
1010
}
1111

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"godb/config"
1717
"godb/db/ent"
1818
"godb/db/ent/ent/gen"
19+
//_ "godb/db/ent/ent/gen/runtime"
1920
gormDB "godb/db/gorm"
2021
"godb/db/sqlboiler"
2122
"godb/db/sqlc"
@@ -71,6 +72,7 @@ func (a *App) Run() {
7172
func (a *App) SetupRouter() {
7273
a.router = chi.NewRouter()
7374
a.router.Use(middleware.Json)
75+
a.router.Use(middleware.Audit)
7476
a.router.NotFound(func(w http.ResponseWriter, r *http.Request) {
7577
w.WriteHeader(http.StatusNotFound)
7678
_, _ = w.Write([]byte(`{"message": "endpoint not found"}`))

middleware/json.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package middleware
22

3-
import "net/http"
3+
import (
4+
"context"
5+
"net/http"
6+
)
47

58
func Json(next http.Handler) http.Handler {
69
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
710
w.Header().Set("Content-Type", "application/json")
811
next.ServeHTTP(w, r)
912
})
1013
}
14+
15+
func Audit(next http.Handler) http.Handler {
16+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
ctx := context.WithValue(r.Context(), "userID", 1)
18+
next.ServeHTTP(w, r.WithContext(ctx))
19+
})
20+
}

0 commit comments

Comments
 (0)