Last active
September 5, 2024 14:26
-
-
Save ra9dev/c38e912a156058cd6ee4558dfacb41db to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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