// Copyright 2022 Harness Inc. All rights reserved. // Use of this source code is governed by the Polyform Free Trial License // that can be found in the LICENSE.md file for this repository. package database import ( "context" "encoding/json" "time" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store/database/dbtx" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/Masterminds/squirrel" "github.com/guregu/null" "github.com/jmoiron/sqlx" "github.com/pkg/errors" ) var _ store.PullReqActivityStore = (*PullReqActivityStore)(nil) // NewPullReqActivityStore returns a new PullReqJournalStore. func NewPullReqActivityStore(db *sqlx.DB) *PullReqActivityStore { return &PullReqActivityStore{ db: db, } } // PullReqActivityStore implements store.PullReqActivityStore backed by a relational database. type PullReqActivityStore struct { db *sqlx.DB } // journal is used to fetch pull request data from the database. // The object should be later re-packed into a different struct to return it as an API response. type pullReqActivity struct { ID int64 `db:"pullreq_activity_id"` Version int64 `db:"pullreq_activity_version"` CreatedBy int64 `db:"pullreq_activity_created_by"` Created int64 `db:"pullreq_activity_created"` Updated int64 `db:"pullreq_activity_updated"` Edited int64 `db:"pullreq_activity_edited"` Deleted null.Int `db:"pullreq_activity_deleted"` ParentID null.Int `db:"pullreq_activity_parent_id"` RepoID int64 `db:"pullreq_activity_repo_id"` PullReqID int64 `db:"pullreq_activity_pullreq_id"` Order int64 `db:"pullreq_activity_order"` SubOrder int64 `db:"pullreq_activity_sub_order"` ReplySeq int64 `db:"pullreq_activity_reply_seq"` Type enum.PullReqActivityType `db:"pullreq_activity_type"` Kind enum.PullReqActivityKind `db:"pullreq_activity_kind"` Text string `db:"pullreq_activity_text"` Payload json.RawMessage `db:"pullreq_activity_payload"` Metadata json.RawMessage `db:"pullreq_activity_metadata"` ResolvedBy null.Int `db:"pullreq_activity_resolved_by"` Resolved null.Int `db:"pullreq_activity_resolved"` AuthorUID string `db:"author_uid"` AuthorName string `db:"author_name"` AuthorEmail string `db:"author_email"` ResolverUID null.String `db:"resolver_uid"` ResolverName null.String `db:"resolver_name"` ResolverEmail null.String `db:"resolver_email"` } const ( pullreqActivityColumns = ` pullreq_activity_id ,pullreq_activity_version ,pullreq_activity_created_by ,pullreq_activity_created ,pullreq_activity_updated ,pullreq_activity_edited ,pullreq_activity_deleted ,pullreq_activity_parent_id ,pullreq_activity_repo_id ,pullreq_activity_pullreq_id ,pullreq_activity_order ,pullreq_activity_sub_order ,pullreq_activity_reply_seq ,pullreq_activity_type ,pullreq_activity_kind ,pullreq_activity_text ,pullreq_activity_payload ,pullreq_activity_metadata ,pullreq_activity_resolved_by ,pullreq_activity_resolved ,author.principal_uid as "author_uid" ,author.principal_display_name as "author_name" ,author.principal_email as "author_email" ,resolver.principal_uid as "resolver_uid" ,resolver.principal_display_name as "resolver_name" ,resolver.principal_email as "resolver_email"` pullreqActivitySelectBase = ` SELECT` + pullreqActivityColumns + ` FROM pullreq_activities INNER JOIN principals author on author.principal_id = pullreq_activity_created_by LEFT JOIN principals resolver on resolver.principal_id = pullreq_activity_resolved_by` ) // Find finds the pull request activity by id. func (s *PullReqActivityStore) Find(ctx context.Context, id int64) (*types.PullReqActivity, error) { const sqlQuery = pullreqActivitySelectBase + ` WHERE pullreq_activity_id = $1` db := dbtx.GetAccessor(ctx, s.db) dst := &pullReqActivity{} if err := db.GetContext(ctx, dst, sqlQuery, id); err != nil { return nil, processSQLErrorf(err, "Failed to find pull request activity") } return mapPullReqActivity(dst), nil } // Create creates a new pull request. func (s *PullReqActivityStore) Create(ctx context.Context, act *types.PullReqActivity) error { const sqlQuery = ` INSERT INTO pullreq_activities ( pullreq_activity_version ,pullreq_activity_created_by ,pullreq_activity_created ,pullreq_activity_updated ,pullreq_activity_edited ,pullreq_activity_deleted ,pullreq_activity_parent_id ,pullreq_activity_repo_id ,pullreq_activity_pullreq_id ,pullreq_activity_order ,pullreq_activity_sub_order ,pullreq_activity_reply_seq ,pullreq_activity_type ,pullreq_activity_kind ,pullreq_activity_text ,pullreq_activity_payload ,pullreq_activity_metadata ,pullreq_activity_resolved_by ,pullreq_activity_resolved ) values ( :pullreq_activity_version ,:pullreq_activity_created_by ,:pullreq_activity_created ,:pullreq_activity_updated ,:pullreq_activity_edited ,:pullreq_activity_deleted ,:pullreq_activity_parent_id ,:pullreq_activity_repo_id ,:pullreq_activity_pullreq_id ,:pullreq_activity_order ,:pullreq_activity_sub_order ,:pullreq_activity_reply_seq ,:pullreq_activity_type ,:pullreq_activity_kind ,:pullreq_activity_text ,:pullreq_activity_payload ,:pullreq_activity_metadata ,:pullreq_activity_resolved_by ,:pullreq_activity_resolved ) RETURNING pullreq_activity_id` db := dbtx.GetAccessor(ctx, s.db) query, arg, err := db.BindNamed(sqlQuery, mapInternalPullReqActivity(act)) if err != nil { return processSQLErrorf(err, "Failed to bind pull request activity object") } if err = db.QueryRowContext(ctx, query, arg...).Scan(&act.ID); err != nil { return processSQLErrorf(err, "Failed to insert pull request activity") } return nil } // Update updates the pull request. func (s *PullReqActivityStore) Update(ctx context.Context, act *types.PullReqActivity) error { const sqlQuery = ` UPDATE pullreq_activities SET pullreq_activity_version = :pullreq_activity_version ,pullreq_activity_updated = :pullreq_activity_updated ,pullreq_activity_edited = :pullreq_activity_edited ,pullreq_activity_deleted = :pullreq_activity_deleted ,pullreq_activity_reply_seq = :pullreq_activity_reply_seq ,pullreq_activity_text = :pullreq_activity_text ,pullreq_activity_payload = :pullreq_activity_payload ,pullreq_activity_metadata = :pullreq_activity_metadata ,pullreq_activity_resolved_by = :pullreq_activity_resolved_by ,pullreq_activity_resolved = :pullreq_activity_resolved WHERE pullreq_activity_id = :pullreq_activity_id AND pullreq_activity_version = :pullreq_activity_version - 1` db := dbtx.GetAccessor(ctx, s.db) updatedAt := time.Now() dbAct := mapInternalPullReqActivity(act) dbAct.Version++ dbAct.Updated = updatedAt.UnixMilli() query, arg, err := db.BindNamed(sqlQuery, dbAct) if err != nil { return processSQLErrorf(err, "Failed to bind pull request activity object") } result, err := db.ExecContext(ctx, query, arg...) if err != nil { return processSQLErrorf(err, "Failed to update pull request activity") } count, err := result.RowsAffected() if err != nil { return processSQLErrorf(err, "Failed to get number of updated rows") } if count == 0 { return store.ErrConflict } act.Version = dbAct.Version act.Updated = dbAct.Updated return nil } // UpdateReplySeq updates the pull request activity's reply sequence. func (s *PullReqActivityStore) UpdateReplySeq(ctx context.Context, act *types.PullReqActivity) (*types.PullReqActivity, error) { for { dup := *act dup.ReplySeq++ err := s.Update(ctx, &dup) if err == nil { return &dup, nil } if !errors.Is(err, store.ErrConflict) { return nil, err } act, err = s.Find(ctx, act.ID) if err != nil { return nil, err } } } // Count of pull requests for a repo. func (s *PullReqActivityStore) Count(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) (int64, error) { stmt := builder. Select("count(*)"). From("pullreq_activities"). Where("pullreq_activity_pullreq_id = ?", prID) if len(opts.Types) == 1 { stmt = stmt.Where("pullreq_activity_type = ?", opts.Types[0]) } else if len(opts.Types) > 1 { stmt = stmt.Where(squirrel.Eq{"pullreq_activity_type": opts.Types}) } if len(opts.Kinds) == 1 { stmt = stmt.Where("pullreq_activity_kind = ?", opts.Kinds[0]) } else if len(opts.Kinds) > 1 { stmt = stmt.Where(squirrel.Eq{"pullreq_activity_kind": opts.Kinds}) } if opts.After != 0 { stmt = stmt.Where("pullreq_created >= ?", opts.After) } if opts.Before != 0 { stmt = stmt.Where("pullreq_created < ?", opts.Before) } sql, args, err := stmt.ToSql() if err != nil { return 0, errors.Wrap(err, "Failed to convert query to sql") } db := dbtx.GetAccessor(ctx, s.db) var count int64 err = db.QueryRowContext(ctx, sql, args...).Scan(&count) if err != nil { return 0, processSQLErrorf(err, "Failed executing count query") } return count, nil } // List returns a list of pull requests for a repo. func (s *PullReqActivityStore) List(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) ([]*types.PullReqActivity, error) { stmt := builder. Select(pullreqActivityColumns). From("pullreq_activities"). InnerJoin("principals author on author.principal_id = pullreq_activity_created_by"). LeftJoin("principals resolver on resolver.principal_id = pullreq_activity_resolved_by"). Where("pullreq_activity_pullreq_id = ?", prID) if len(opts.Types) == 1 { stmt = stmt.Where("pullreq_activity_type = ?", opts.Types[0]) } else if len(opts.Types) > 1 { stmt = stmt.Where(squirrel.Eq{"pullreq_activity_type": opts.Types}) } if len(opts.Kinds) == 1 { stmt = stmt.Where("pullreq_activity_kind = ?", opts.Kinds[0]) } else if len(opts.Kinds) > 1 { stmt = stmt.Where(squirrel.Eq{"pullreq_activity_kind": opts.Kinds}) } if opts.After != 0 { stmt = stmt.Where("pullreq_created >= ?", opts.After) } if opts.Before != 0 { stmt = stmt.Where("pullreq_created < ?", opts.Before) } if opts.Limit > 0 { stmt = stmt.Limit(uint64(limit(opts.Limit))) } stmt = stmt.OrderBy("pullreq_activity_order asc", "pullreq_activity_sub_order asc") sql, args, err := stmt.ToSql() if err != nil { return nil, errors.Wrap(err, "Failed to convert pull request activity query to sql") } dst := make([]*pullReqActivity, 0) db := dbtx.GetAccessor(ctx, s.db) if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { return nil, processSQLErrorf(err, "Failed executing pull request activity list query") } return mapSlicePullReqActivity(dst), nil } func mapPullReqActivity(act *pullReqActivity) *types.PullReqActivity { m := &types.PullReqActivity{ ID: act.ID, Version: act.Version, CreatedBy: act.CreatedBy, Created: act.Created, Updated: act.Updated, Edited: act.Edited, Deleted: act.Deleted.Ptr(), ParentID: act.ParentID.Ptr(), RepoID: act.RepoID, PullReqID: act.PullReqID, Order: act.Order, SubOrder: act.SubOrder, ReplySeq: act.ReplySeq, Type: act.Type, Kind: act.Kind, Text: act.Text, Payload: make(map[string]interface{}), Metadata: make(map[string]interface{}), ResolvedBy: act.ResolvedBy.Ptr(), Resolved: act.Resolved.Ptr(), Author: types.PrincipalInfo{}, Resolver: nil, } m.Author = types.PrincipalInfo{ ID: act.CreatedBy, UID: act.AuthorUID, Name: act.AuthorName, Email: act.AuthorEmail, } _ = json.Unmarshal(act.Payload, &m.Payload) _ = json.Unmarshal(act.Metadata, &m.Metadata) if act.ResolvedBy.Valid { m.Resolver = &types.PrincipalInfo{ ID: act.ResolvedBy.Int64, UID: act.ResolverUID.String, Name: act.ResolverName.String, Email: act.ResolverEmail.String, } } return m } func mapInternalPullReqActivity(act *types.PullReqActivity) *pullReqActivity { m := &pullReqActivity{ ID: act.ID, Version: act.Version, CreatedBy: act.CreatedBy, Created: act.Created, Updated: act.Updated, Edited: act.Edited, Deleted: null.IntFromPtr(act.Deleted), ParentID: null.IntFromPtr(act.ParentID), RepoID: act.RepoID, PullReqID: act.PullReqID, Order: act.Order, SubOrder: act.SubOrder, ReplySeq: act.ReplySeq, Type: act.Type, Kind: act.Kind, Text: act.Text, Payload: nil, Metadata: nil, ResolvedBy: null.IntFromPtr(act.ResolvedBy), Resolved: null.IntFromPtr(act.Resolved), } m.Payload, _ = json.Marshal(act.Payload) m.Metadata, _ = json.Marshal(act.Metadata) return m } func mapSlicePullReqActivity(a []*pullReqActivity) []*types.PullReqActivity { m := make([]*types.PullReqActivity, len(a)) for i, act := range a { m[i] = mapPullReqActivity(act) } return m }