Skip to content

Instantly share code, notes, and snippets.

@ra9dev
Last active September 5, 2024 14:26
Show Gist options
  • Select an option

  • Save ra9dev/c38e912a156058cd6ee4558dfacb41db to your computer and use it in GitHub Desktop.

Select an option

Save ra9dev/c38e912a156058cd6ee4558dfacb41db to your computer and use it in GitHub Desktop.
package postgres
import (
"context"
"encoding/json"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"internal/datastore"
"internal/entity"
)
const (
queryCreateContent = `INSERT INTO content (
id,
type,
attributes
) VALUES (
:id,
:type,
:attributes
)`
queryUpdateContent = `UPDATE content SET
type = :type,
attributes = :attributes,
updated_at = now()
WHERE id = :id`
queryDeleteContent = `DELETE FROM content where id = :id`
)
var _ datastore.ContentRepo = (*ContentRepo)(nil)
var psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
type Content struct {
ID uuid.UUID `json:"id" db:"id"`
Type entity.ContentType `json:"type" db:"type"`
Attributes []byte `json:"attributes" db:"attributes"`
entity.MutableRecord
}
func NewContent(source entity.Content) (Content, error) {
attributes, err := json.Marshal(source.Attributes)
if err != nil {
return Content{}, fmt.Errorf("failed to marshal attributes: %w", err)
}
return Content{
ID: source.ID,
Type: source.Type,
Attributes: attributes,
MutableRecord: source.MutableRecord,
}, nil
}
func (c Content) ToEntity() (entity.Content, error) {
var attributes map[string]any
if err := json.Unmarshal(c.Attributes, &attributes); err != nil {
return entity.Content{}, fmt.Errorf("failed to unmarshal attributes: %w", err)
}
return entity.Content{
ID: c.ID,
Type: c.Type,
Attributes: attributes,
MutableRecord: c.MutableRecord,
}, nil
}
type ContentRepo struct {
crud CRUD[entity.Content, Content, entity.ContentFilter]
exec sqlx.ExtContext
}
func NewContentRepo(exec sqlx.ExtContext) ContentRepo {
repo := ContentRepo{
exec: exec,
}
queries := CRUDQueries[entity.ContentFilter]{
createQuery: queryCreateContent,
selectQueryFunc: repo.selectQuery,
updateQuery: queryUpdateContent,
deleteQuery: queryDeleteContent,
}
repo.crud = NewCRUD(exec, queries, NewContent)
return repo
}
func (c ContentRepo) Create(ctx context.Context, in entity.Content) (entity.Content, error) {
in.ID = uuid.New()
var err error
in, _, err = c.crud.Create(ctx, in)
if err != nil {
return in, err
}
return in, nil
}
func (c ContentRepo) selectQuery(filter entity.ContentFilter) sq.SelectBuilder {
qb := psql.
Select(
"id",
"type",
"attributes",
"created_at",
"updated_at",
).
From("content")
if len(filter.IDs) > 0 {
qb = qb.Where(sq.Eq{"id": filter.IDs})
}
if len(filter.Types) > 0 {
qb = qb.Where(sq.Eq{"type": filter.Types})
}
return qb
}
func (c ContentRepo) All(ctx context.Context, filter entity.ContentFilter) ([]entity.Content, error) {
return c.crud.All(ctx, filter)
}
func (c ContentRepo) One(ctx context.Context, filter entity.ContentFilter) (entity.Content, error) {
return c.crud.One(ctx, filter)
}
func (c ContentRepo) Update(ctx context.Context, in entity.Content) (entity.Content, error) {
var err error
in, err = c.crud.Update(ctx, in)
if err != nil {
return in, err
}
in.UpdatedAt = time.Now().UTC()
return in, nil
}
func (c ContentRepo) Delete(ctx context.Context, in entity.Content) error {
return c.crud.Delete(ctx, in)
}
package postgres
import (
"context"
"fmt"
"strings"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
const returningPart = "returning"
var ErrNoRecords = errors.New("no records found")
type (
EntityShifter[Entity any] interface {
ToEntity() (Entity, error)
}
CRUDQueries[Filter any] struct {
createQuery string
selectQueryFunc func(filter Filter) sq.SelectBuilder
updateQuery string
deleteQuery string
}
CRUD[Entity any, DTO EntityShifter[Entity], Filter any] struct {
queries CRUDQueries[Filter]
dtoConstructFunc func(in Entity) (DTO, error)
exec sqlx.ExtContext
}
)
func NewCRUD[Entity any, DTO EntityShifter[Entity], Filter any](
exec sqlx.ExtContext,
queries CRUDQueries[Filter],
dtoConstructFunc func(in Entity) (DTO, error),
) CRUD[Entity, DTO, Filter] {
return CRUD[Entity, DTO, Filter]{
queries: queries,
exec: exec,
dtoConstructFunc: dtoConstructFunc,
}
}
func hasReturningInQuery(query string) bool {
return strings.Contains(strings.ToLower(query), returningPart)
}
func (c CRUD[Entity, DTO, Filter]) Create(ctx context.Context, in Entity) (Entity, int64, error) {
query := c.exec.Rebind(c.queries.createQuery)
dto, err := c.dtoConstructFunc(in)
if err != nil {
return in, 0, err
}
var lastInsertId int64
if hasReturningInQuery(query) {
query, args, err := c.exec.BindNamed(query, dto)
if err != nil {
return in, 0, ToDatastoreError(err)
}
err = sqlx.GetContext(ctx, c.exec, &lastInsertId, query, args...)
if err != nil {
return in, 0, ToDatastoreError(err)
}
return in, lastInsertId, nil
}
_, err = sqlx.NamedExecContext(ctx, c.exec, query, dto)
if err != nil {
return in, 0, ToDatastoreError(err)
}
return in, lastInsertId, nil
}
func (c CRUD[Entity, DTO, Filter]) All(ctx context.Context, filter Filter) ([]Entity, error) {
qb := c.queries.selectQueryFunc(filter)
query, args, err := qb.ToSql()
if err != nil {
return nil, fmt.Errorf("failed to parse qb to sql: %w", err)
}
dto := make([]DTO, 0)
if err = sqlx.SelectContext(ctx, c.exec, &dto, query, args...); err != nil {
return nil, ToDatastoreError(err)
}
output := make([]Entity, 0, len(dto))
for _, item := range dto {
var out Entity
out, err = item.ToEntity()
if err != nil {
return nil, err // nolint
}
output = append(output, out)
}
return output, nil
}
func (c CRUD[Entity, DTO, Filter]) One(ctx context.Context, filter Filter) (Entity, error) {
var (
qb = c.queries.selectQueryFunc(filter)
out Entity
dto DTO
)
query, args, err := qb.ToSql()
if err != nil {
return out, fmt.Errorf("failed to parse qb to sql: %w", err)
}
if err = sqlx.GetContext(ctx, c.exec, &dto, query, args...); err != nil {
return out, ToDatastoreError(err)
}
return dto.ToEntity() // nolint
}
func (c CRUD[Entity, DTO, Filter]) Update(ctx context.Context, in Entity) (Entity, error) {
query := c.exec.Rebind(c.queries.updateQuery)
dto, err := c.dtoConstructFunc(in)
if err != nil {
return in, err
}
result, err := sqlx.NamedExecContext(ctx, c.exec, query, dto)
if err != nil {
return in, ToDatastoreError(err)
}
affected, err := result.RowsAffected()
if err != nil {
return in, ToDatastoreError(err)
}
if affected == 0 {
return in, ErrNoRecords
}
return in, nil
}
func (c CRUD[Entity, DTO, Filter]) Delete(ctx context.Context, in Entity) error {
query := c.exec.Rebind(c.queries.deleteQuery)
result, err := sqlx.NamedExecContext(ctx, c.exec, query, in)
if err != nil {
return ToDatastoreError(err)
}
affected, err := result.RowsAffected()
if err != nil {
return ToDatastoreError(err)
}
if affected == 0 {
return ErrNoRecords
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment