first working version
This commit is contained in:
163
repo.go
Normal file
163
repo.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInitRepo = errors.New("error creating table")
|
||||
ErrMarshal = errors.New("error marshal")
|
||||
ErrExecQuery = errors.New("error executing DB query")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type Repo[T any] interface {
|
||||
// Create inserts object into repository table. If id already exists
|
||||
// in the database it is an error.
|
||||
Create(ctx context.Context, id string, v *T) error
|
||||
// Read returns object with specified id or ErrNotFound
|
||||
Read(ctx context.Context, id string) (*T, error)
|
||||
// Update updates object with id.
|
||||
Update(ctx context.Context, id string, v *T) error
|
||||
// Delete performs soft-delete (actually marks object as unavailable).
|
||||
Delete(ctx context.Context, id string) error
|
||||
// Purge actually deletes database record with id.
|
||||
Purge(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// OpenOrCreate accepts *sql.DB and tablename and tries to create the named table
|
||||
// in the database if it does not exist. The DB instance will also be used by the repository.
|
||||
func OpenOrCreate[T any](ctx context.Context, db *sql.DB, tablename string) (Repo[T], error) {
|
||||
if err := initDB(ctx, db, tablename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &repo[T]{
|
||||
db: db,
|
||||
table: tablename,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type repo[T any] struct {
|
||||
db *sql.DB
|
||||
table string
|
||||
}
|
||||
|
||||
func (r *repo[T]) Create(ctx context.Context, id string, v *T) error {
|
||||
now := time.Now()
|
||||
|
||||
b, err := marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "INSERT INTO " + r.table + " (id, created_at, updated_at, payload) VALUES ($1, $2, $3, $4)"
|
||||
|
||||
if err := r.execContext(ctx, query, id, now, now, string(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo[T]) Read(ctx context.Context, id string) (*T, error) {
|
||||
query := "SELECT payload FROM " + r.table + " WHERE id = $1 AND deleted_at is NULL"
|
||||
|
||||
row := r.db.QueryRowContext(ctx, query, id)
|
||||
|
||||
var s string
|
||||
|
||||
err := row.Scan(&s)
|
||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, errors.Join(ErrNotFound, err)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := unmarshal[T]([]byte(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (r *repo[T]) Update(ctx context.Context, id string, v *T) error {
|
||||
now := time.Now()
|
||||
|
||||
b, err := marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := "UPDATE " + r.table + " SET updated_at = $1, payload = $2 WHERE id = $3 AND deleted_at IS NULL"
|
||||
|
||||
if err := r.execContext(ctx, query, now, string(b), id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete performs soft-delete. It just marks the record as deleted and it will
|
||||
// no longer be available to Read and Update methods.
|
||||
func (r *repo[T]) Delete(ctx context.Context, id string) error {
|
||||
now := time.Now()
|
||||
|
||||
query := "UPDATE " + r.table + " SET deleted_at = $1 WHERE id = $2 AND deleted_at IS NULL"
|
||||
|
||||
if err := r.execContext(ctx, query, now, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo[T]) Purge(ctx context.Context, id string) error {
|
||||
query := "DELETE FROM " + r.table + " WHERE id = $1"
|
||||
|
||||
if err := r.execContext(ctx, query, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo[T]) execContext(ctx context.Context, query string, args ...any) error {
|
||||
res, err := r.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return errors.Join(ErrExecQuery, err)
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Join(ErrExecQuery, err)
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshal(v any) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrMarshal, err)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func unmarshal[T any](b []byte) (*T, error) {
|
||||
v := new(T)
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return nil, errors.Join(ErrMarshal, err)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
Reference in New Issue
Block a user