219 lines
5.7 KiB
Go
219 lines
5.7 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/lib/pq"
|
|
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/port"
|
|
)
|
|
|
|
// Compile-time interface check.
|
|
var _ port.PersonaRepository = (*PersonaRepository)(nil)
|
|
|
|
// personaRow maps to the personas table.
|
|
type personaRow struct {
|
|
ID string `db:"id"`
|
|
Name string `db:"name"`
|
|
Handle string `db:"handle"`
|
|
Gender string `db:"gender"`
|
|
Description string `db:"description"`
|
|
Tags pq.StringArray `db:"tags"`
|
|
SpecJSON []byte `db:"spec_json"`
|
|
AnchorURL *string `db:"anchor_url"`
|
|
AvatarURL *string `db:"avatar_url"`
|
|
BannerURL *string `db:"banner_url"`
|
|
ImageURLs pq.StringArray `db:"image_urls"`
|
|
VideoURLs pq.StringArray `db:"video_urls"`
|
|
Status string `db:"status"`
|
|
CreatedAt time.Time `db:"created_at"`
|
|
}
|
|
|
|
func (r *personaRow) toDomain() *domain.Persona {
|
|
p := &domain.Persona{
|
|
ID: domain.PersonaID(r.ID),
|
|
Name: r.Name,
|
|
Handle: r.Handle,
|
|
Gender: r.Gender,
|
|
Description: r.Description,
|
|
Tags: []string(r.Tags),
|
|
Status: domain.PersonaStatus(r.Status),
|
|
CreatedAt: r.CreatedAt,
|
|
ImageURLs: []string(r.ImageURLs),
|
|
VideoURLs: []string(r.VideoURLs),
|
|
}
|
|
if p.Tags == nil {
|
|
p.Tags = []string{}
|
|
}
|
|
if p.ImageURLs == nil {
|
|
p.ImageURLs = []string{}
|
|
}
|
|
if p.VideoURLs == nil {
|
|
p.VideoURLs = []string{}
|
|
}
|
|
if len(r.SpecJSON) > 0 {
|
|
p.SpecJSON = json.RawMessage(r.SpecJSON)
|
|
}
|
|
if r.AnchorURL != nil {
|
|
p.AnchorURL = *r.AnchorURL
|
|
}
|
|
if r.AvatarURL != nil {
|
|
p.AvatarURL = *r.AvatarURL
|
|
}
|
|
if r.BannerURL != nil {
|
|
p.BannerURL = *r.BannerURL
|
|
}
|
|
return p
|
|
}
|
|
|
|
// PersonaRepository implements port.PersonaRepository with PostgreSQL/CockroachDB.
|
|
type PersonaRepository struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewPersonaRepository creates a new Postgres-backed persona repository.
|
|
func NewPersonaRepository(db *sqlx.DB) *PersonaRepository {
|
|
return &PersonaRepository{db: db}
|
|
}
|
|
|
|
func (r *PersonaRepository) Create(ctx context.Context, persona *domain.Persona) error {
|
|
var specJSON []byte
|
|
if persona.SpecJSON != nil {
|
|
specJSON = []byte(persona.SpecJSON)
|
|
}
|
|
|
|
var anchorURL, avatarURL, bannerURL *string
|
|
if persona.AnchorURL != "" {
|
|
anchorURL = &persona.AnchorURL
|
|
}
|
|
if persona.AvatarURL != "" {
|
|
avatarURL = &persona.AvatarURL
|
|
}
|
|
if persona.BannerURL != "" {
|
|
bannerURL = &persona.BannerURL
|
|
}
|
|
|
|
err := r.db.QueryRowContext(ctx, `
|
|
INSERT INTO personas (name, handle, gender, description, tags, spec_json, anchor_url, avatar_url, banner_url, image_urls, video_urls, status, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
RETURNING id
|
|
`,
|
|
persona.Name,
|
|
persona.Handle,
|
|
persona.Gender,
|
|
persona.Description,
|
|
pq.StringArray(persona.Tags),
|
|
specJSON,
|
|
anchorURL,
|
|
avatarURL,
|
|
bannerURL,
|
|
pq.StringArray(persona.ImageURLs),
|
|
pq.StringArray(persona.VideoURLs),
|
|
string(persona.Status),
|
|
persona.CreatedAt,
|
|
).Scan(&persona.ID)
|
|
if err != nil {
|
|
if isUniqueViolation(err) {
|
|
return domain.ErrDuplicateHandle
|
|
}
|
|
return fmt.Errorf("insert persona: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PersonaRepository) GetByID(ctx context.Context, id domain.PersonaID) (*domain.Persona, error) {
|
|
var row personaRow
|
|
err := r.db.GetContext(ctx, &row, `
|
|
SELECT id, name, handle, gender, description, tags, spec_json, anchor_url, avatar_url, banner_url, image_urls, video_urls, status, created_at
|
|
FROM personas WHERE id = $1
|
|
`, string(id))
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, domain.ErrPersonaNotFound
|
|
}
|
|
return nil, fmt.Errorf("get persona: %w", err)
|
|
}
|
|
return row.toDomain(), nil
|
|
}
|
|
|
|
func (r *PersonaRepository) List(ctx context.Context, limit, offset int) ([]*domain.Persona, error) {
|
|
var rows []personaRow
|
|
err := r.db.SelectContext(ctx, &rows, `
|
|
SELECT id, name, handle, gender, description, tags, spec_json, anchor_url, avatar_url, banner_url, image_urls, video_urls, status, created_at
|
|
FROM personas
|
|
ORDER BY created_at DESC
|
|
LIMIT $1 OFFSET $2
|
|
`, limit, offset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list personas: %w", err)
|
|
}
|
|
|
|
result := make([]*domain.Persona, len(rows))
|
|
for i := range rows {
|
|
result[i] = rows[i].toDomain()
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *PersonaRepository) Update(ctx context.Context, persona *domain.Persona) error {
|
|
var specJSON []byte
|
|
if persona.SpecJSON != nil {
|
|
specJSON = []byte(persona.SpecJSON)
|
|
}
|
|
|
|
var anchorURL, avatarURL, bannerURL *string
|
|
if persona.AnchorURL != "" {
|
|
anchorURL = &persona.AnchorURL
|
|
}
|
|
if persona.AvatarURL != "" {
|
|
avatarURL = &persona.AvatarURL
|
|
}
|
|
if persona.BannerURL != "" {
|
|
bannerURL = &persona.BannerURL
|
|
}
|
|
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE personas
|
|
SET name = $2, handle = $3, gender = $4, description = $5, tags = $6,
|
|
spec_json = $7, anchor_url = $8, avatar_url = $9, banner_url = $10,
|
|
image_urls = $11, video_urls = $12, status = $13
|
|
WHERE id = $1
|
|
`,
|
|
string(persona.ID),
|
|
persona.Name,
|
|
persona.Handle,
|
|
persona.Gender,
|
|
persona.Description,
|
|
pq.StringArray(persona.Tags),
|
|
specJSON,
|
|
anchorURL,
|
|
avatarURL,
|
|
bannerURL,
|
|
pq.StringArray(persona.ImageURLs),
|
|
pq.StringArray(persona.VideoURLs),
|
|
string(persona.Status),
|
|
)
|
|
if err != nil {
|
|
if isUniqueViolation(err) {
|
|
return domain.ErrDuplicateHandle
|
|
}
|
|
return fmt.Errorf("update persona: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("update persona rows affected: %w", err)
|
|
}
|
|
if rows == 0 {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
return nil
|
|
}
|