package repository import ( "context" "database/sql" "errors" "fmt" "log/slog" "strings" "time" "code.capytal.cc/capytal/comicverse/model" "code.capytal.cc/loreddev/x/tinyssert" "github.com/google/uuid" ) type Publication struct { baseRepostiory } func NewPublication(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyssert.Assertions) (*Publication, error) { b := newBaseRepostiory(ctx, db, log, assert) tx, err := db.BeginTx(ctx, nil) if err != nil { return nil, err } _, err = tx.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS publications ( id TEXT NOT NULL PRIMARY KEY, title TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL )`) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, errors.Join(errors.New("unable to create publication tables"), err) } return &Publication{baseRepostiory: b}, nil } func (repo Publication) Create(p model.Publication) error { repo.assert.NotNil(repo.db) repo.assert.NotNil(repo.ctx) repo.assert.NotNil(repo.ctx) if err := p.Validate(); err != nil { return errors.Join(ErrInvalidInput, err) } tx, err := repo.db.BeginTx(repo.ctx, nil) if err != nil { return errors.Join(ErrDatabaseConn, err) } q := ` INSERT INTO publications (id, title, created_at, updated_at) VALUES (:id, :title, :created_at, :updated_at) ` log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q)) log.DebugContext(repo.ctx, "Inserting new publication") _, err = tx.ExecContext(repo.ctx, q, sql.Named("id", p.ID), sql.Named("title", p.Title), sql.Named("created_at", p.DateCreated.Format(dateFormat)), sql.Named("updated_at", p.DateUpdated.Format(dateFormat)), ) if err != nil { log.ErrorContext(repo.ctx, "Failed to insert publication", slog.String("error", err.Error())) return errors.Join(ErrExecuteQuery, err) } if err := tx.Commit(); err != nil { log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error())) return errors.Join(ErrCommitQuery, err) } return nil } func (repo Publication) GetByID(publicationID uuid.UUID) (publication model.Publication, err error) { repo.assert.NotNil(repo.db) repo.assert.NotNil(repo.ctx) repo.assert.NotNil(repo.log) q := ` SELECT id, title, created_at, updated_at FROM publications WHERE id = :id ` log := repo.log.With(slog.String("query", q), slog.String("id", publicationID.String())) log.DebugContext(repo.ctx, "Getting publication by ID") row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("id", publicationID)) var id uuid.UUID var title string var dateCreatedStr, dateUpdatedStr string err = row.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr) if err != nil { log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error())) return model.Publication{}, errors.Join(ErrInvalidOutput, err) } dateCreated, err := time.Parse(dateFormat, dateCreatedStr) if err != nil { log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error())) return model.Publication{}, errors.Join(ErrInvalidOutput, err) } dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr) if err != nil { log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error())) return model.Publication{}, errors.Join(ErrInvalidOutput, err) } return model.Publication{ ID: id, Title: title, DateCreated: dateCreated, DateUpdated: dateUpdated, }, nil } func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publication, err error) { repo.assert.NotNil(repo.db) repo.assert.NotNil(repo.ctx) repo.assert.NotNil(repo.log) // Begin tx so we don't read rows as they are being updated tx, err := repo.db.BeginTx(repo.ctx, nil) if err != nil { return nil, errors.Join(ErrDatabaseConn, err) } c := make([]string, len(ids)) for i, id := range ids { c[i] = fmt.Sprintf("id = '%s'", id.String()) } q := fmt.Sprintf(` SELECT id, title, created_at, updated_at FROM publications WHERE %s `, strings.Join(c, " OR ")) log := repo.log.With(slog.String("query", q)) log.DebugContext(repo.ctx, "Getting publications by IDs") rows, err := tx.QueryContext(repo.ctx, q) if err != nil { log.ErrorContext(repo.ctx, "Failed to get publications by IDs", slog.String("error", err.Error())) return nil, errors.Join(ErrExecuteQuery, err) } defer func() { err = rows.Close() if err != nil { err = errors.Join(ErrCloseConn, err) } }() ps := []model.Publication{} for rows.Next() { var id uuid.UUID var title string var dateCreatedStr, dateUpdatedStr string err := rows.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr) if err != nil { log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error())) return nil, errors.Join(ErrInvalidOutput, err) } dateCreated, err := time.Parse(dateFormat, dateCreatedStr) if err != nil { log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error())) return nil, errors.Join(ErrInvalidOutput, err) } dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr) if err != nil { log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error())) return nil, errors.Join(ErrInvalidOutput, err) } ps = append(ps, model.Publication{ ID: id, Title: title, DateCreated: dateCreated, DateUpdated: dateUpdated, }) } if err := tx.Commit(); err != nil { log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error())) return nil, errors.Join(ErrCommitQuery, err) } return ps, nil } func (repo Publication) Update(p model.Publication) error { repo.assert.NotNil(repo.db) repo.assert.NotNil(repo.ctx) repo.assert.NotNil(repo.ctx) if err := p.Validate(); err != nil { return errors.Join(ErrInvalidInput, err) } tx, err := repo.db.BeginTx(repo.ctx, nil) if err != nil { return errors.Join(ErrDatabaseConn, err) } q := ` UPDATE publications SET title = :title updated_at = :updated_at WHERE id = :id ` log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q)) log.DebugContext(repo.ctx, "Updating publication") _, err = tx.ExecContext(repo.ctx, q, sql.Named("title", p.Title), sql.Named("updated_at", p.DateUpdated.Format(dateFormat)), sql.Named("id", p.ID), ) if err != nil { log.ErrorContext(repo.ctx, "Failed to insert publication", slog.String("error", err.Error())) return errors.Join(ErrExecuteQuery, err) } if err := tx.Commit(); err != nil { log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error())) return errors.Join(ErrCommitQuery, err) } return nil } func (repo Publication) DeleteByID(id uuid.UUID) error { repo.assert.NotNil(repo.db) repo.assert.NotNil(repo.ctx) repo.assert.NotNil(repo.ctx) tx, err := repo.db.BeginTx(repo.ctx, nil) if err != nil { return err } q := ` DELETE FROM publications WHERE id = :id ` log := repo.log.With(slog.String("id", id.String()), slog.String("query", q)) log.DebugContext(repo.ctx, "Deleting publication") _, err = tx.ExecContext(repo.ctx, q, sql.Named("id", id)) if err != nil { log.ErrorContext(repo.ctx, "Failed to delete publication", slog.String("error", err.Error())) return errors.Join(ErrExecuteQuery, err) } if err := tx.Commit(); err != nil { log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error())) return errors.Join(ErrCommitQuery, err) } return nil }