mirror of
https://github.com/harness/drone.git
synced 2025-05-04 09:52:08 +08:00
380 lines
11 KiB
Go
380 lines
11 KiB
Go
// Copyright 2021 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"
|
|
"fmt"
|
|
|
|
"github.com/harness/gitness/internal/paths"
|
|
"github.com/harness/gitness/internal/store"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/check"
|
|
"github.com/harness/gitness/types/enum"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// Creates a new alias path (Don't call this for new path creation!)
|
|
func CreateAliasPath(ctx context.Context, db *sqlx.DB, path *types.Path) error {
|
|
if !path.IsAlias {
|
|
return store.ErrAliasPathRequired
|
|
}
|
|
|
|
// ensure path length is okay
|
|
if check.PathTooLong(path.Value, path.TargetType == enum.PathTargetTypeSpace) {
|
|
log.Warn().Msgf("Path '%s' is too long.", path.Value)
|
|
return store.ErrPathTooLong
|
|
}
|
|
|
|
query, arg, err := db.BindNamed(pathInsert, path)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to bind path object")
|
|
}
|
|
|
|
if err = db.QueryRowContext(ctx, query, arg...).Scan(&path.ID); err != nil {
|
|
return processSqlErrorf(err, "Insert query failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Creates a new path as part of a transaction
|
|
func CreatePathTx(ctx context.Context, db *sqlx.DB, tx *sqlx.Tx, path *types.Path) error {
|
|
|
|
// ensure path length is okay
|
|
if check.PathTooLong(path.Value, path.TargetType == enum.PathTargetTypeSpace) {
|
|
log.Warn().Msgf("Path '%s' is too long.", path.Value)
|
|
return store.ErrPathTooLong
|
|
}
|
|
|
|
// In case it's not an alias, ensure there are no duplicates
|
|
if !path.IsAlias {
|
|
if cnt, err := CountPathsTx(ctx, tx, path.TargetType, path.TargetId); err != nil {
|
|
return err
|
|
} else if cnt > 0 {
|
|
return store.ErrPrimaryPathAlreadyExists
|
|
}
|
|
}
|
|
|
|
query, arg, err := db.BindNamed(pathInsert, path)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to bind path object")
|
|
}
|
|
|
|
if err = tx.QueryRowContext(ctx, query, arg...).Scan(&path.ID); err != nil {
|
|
return processSqlErrorf(err, "Insert query failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func CountPrimaryChildPathsTx(ctx context.Context, tx *sqlx.Tx, prefix string) (int64, error) {
|
|
var count int64
|
|
err := tx.QueryRowContext(ctx, pathCountPrimaryForPrefix, paths.Concatinate(prefix, "%")).Scan(&count)
|
|
if err != nil {
|
|
return 0, processSqlErrorf(err, "Count query failed")
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func ListPrimaryChildPathsTx(ctx context.Context, tx *sqlx.Tx, prefix string) ([]*types.Path, error) {
|
|
childs := []*types.Path{}
|
|
|
|
if err := tx.SelectContext(ctx, &childs, pathSelectPrimaryForPrefix, paths.Concatinate(prefix, "%")); err != nil {
|
|
return nil, processSqlErrorf(err, "Select query failed")
|
|
}
|
|
|
|
return childs, nil
|
|
}
|
|
|
|
// Replaces the path for a target as part of a transaction - keeps the existing as alias if requested.
|
|
func ReplacePathTx(ctx context.Context, db *sqlx.DB, tx *sqlx.Tx, path *types.Path, keepAsAlias bool) error {
|
|
|
|
if path.IsAlias {
|
|
return store.ErrPrimaryPathRequired
|
|
}
|
|
|
|
// ensure new path length is okay
|
|
if check.PathTooLong(path.Value, path.TargetType == enum.PathTargetTypeSpace) {
|
|
log.Warn().Msgf("Path '%s' is too long.", path.Value)
|
|
return store.ErrPathTooLong
|
|
}
|
|
|
|
// existing is always non-alias (as query filters for IsAlias=0)
|
|
existing := new(types.Path)
|
|
err := tx.GetContext(ctx, existing, pathSelectPrimaryForTarget, string(path.TargetType), fmt.Sprint(path.TargetId))
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to get the existing primary path")
|
|
}
|
|
|
|
// Only look for childs if the type can have childs
|
|
if path.TargetType == enum.PathTargetTypeSpace {
|
|
|
|
// get all primary paths that start with the current path before updating (or we can run into recursion)
|
|
childs, err := ListPrimaryChildPathsTx(ctx, tx, existing.Value)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Failed to get primary child paths for '%s'", existing.Value)
|
|
}
|
|
|
|
for _, child := range childs {
|
|
// create path with updated path (child already is primary)
|
|
updatedChild := new(types.Path)
|
|
*updatedChild = *child
|
|
updatedChild.ID = 0 // will be regenerated
|
|
updatedChild.Created = path.Created
|
|
updatedChild.Updated = path.Updated
|
|
updatedChild.CreatedBy = path.CreatedBy
|
|
updatedChild.Value = path.Value + updatedChild.Value[len(existing.Value):]
|
|
|
|
// ensure new child path length is okay
|
|
if check.PathTooLong(updatedChild.Value, path.TargetType == enum.PathTargetTypeSpace) {
|
|
log.Warn().Msgf("Path '%s' is too long.", path.Value)
|
|
return store.ErrPathTooLong
|
|
}
|
|
|
|
query, arg, err := db.BindNamed(pathInsert, updatedChild)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to bind path object")
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, query, arg...)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to create new primary child path '%s'", updatedChild.Value)
|
|
}
|
|
|
|
// make current child an alias or delete it
|
|
if keepAsAlias {
|
|
_, err = tx.ExecContext(ctx, pathMakeAlias, child.ID)
|
|
} else {
|
|
_, err = tx.ExecContext(ctx, pathDeleteId, child.ID)
|
|
}
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to mark existing child path '%s' as alias", updatedChild.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// insert the new Path
|
|
query, arg, err := db.BindNamed(pathInsert, path)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to bind path object")
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, query, arg...)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to create new primary path '%s'", path.Value)
|
|
}
|
|
|
|
// make existing an alias
|
|
if keepAsAlias {
|
|
_, err = tx.ExecContext(ctx, pathMakeAlias, existing.ID)
|
|
} else {
|
|
_, err = tx.ExecContext(ctx, pathDeleteId, existing.ID)
|
|
}
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to mark existing path '%s' as alias", existing.Value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Finds the primary path for a target.
|
|
func FindPathTx(ctx context.Context, tx *sqlx.Tx, targetType enum.PathTargetType, targetId int64) (*types.Path, error) {
|
|
dst := new(types.Path)
|
|
err := tx.GetContext(ctx, dst, pathSelectPrimaryForTarget, string(targetType), fmt.Sprint(targetId))
|
|
if err != nil {
|
|
return nil, processSqlErrorf(err, "Select query failed")
|
|
}
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
// Deletes a specific path alias (primary can't be deleted, only with delete all).
|
|
func DeletePath(ctx context.Context, db *sqlx.DB, id int64) error {
|
|
tx, err := db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return processSqlErrorf(err, "Failed to start a new transaction")
|
|
}
|
|
defer func(tx *sqlx.Tx) {
|
|
_ = tx.Rollback()
|
|
}(tx)
|
|
|
|
// ensure path is an alias
|
|
dst := new(types.Path)
|
|
if err = tx.GetContext(ctx, dst, pathSelectId, id); err != nil {
|
|
return processSqlErrorf(err, "Failed to find path with id %d", id)
|
|
} else if !dst.IsAlias {
|
|
return store.ErrPrimaryPathCantBeDeleted
|
|
}
|
|
|
|
// delete the path
|
|
if _, err = tx.ExecContext(ctx, pathDeleteId, id); err != nil {
|
|
return processSqlErrorf(err, "Delete query failed")
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
return processSqlErrorf(err, "Failed to commit transaction")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Deletes all paths for a target as part of a transaction.
|
|
func DeleteAllPaths(ctx context.Context, tx *sqlx.Tx, targetType enum.PathTargetType, targetId int64) error {
|
|
// delete all entries for the target
|
|
if _, err := tx.ExecContext(ctx, pathDeleteTarget, string(targetType), fmt.Sprint(targetId)); err != nil {
|
|
return processSqlErrorf(err, "Query for deleting all pahts failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Lists all paths for a target.
|
|
func ListPaths(ctx context.Context, db *sqlx.DB, targetType enum.PathTargetType, targetId int64, opts *types.PathFilter) ([]*types.Path, error) {
|
|
dst := []*types.Path{}
|
|
|
|
// if the user does not provide any customer filter
|
|
// or sorting we use the default select statement.
|
|
if opts.Sort == enum.PathAttrNone {
|
|
err := db.SelectContext(ctx, &dst, pathSelect, string(targetType), fmt.Sprint(targetId), limit(opts.Size), offset(opts.Page, opts.Size))
|
|
if err != nil {
|
|
return nil, processSqlErrorf(err, "Default select query failed")
|
|
}
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
// else we construct the sql statement.
|
|
stmt := builder.Select("*").From("paths").Where("path_targetType = $1 AND path_targetId = $2", string(targetType), fmt.Sprint(targetId))
|
|
stmt = stmt.Limit(uint64(limit(opts.Size)))
|
|
stmt = stmt.Offset(uint64(offset(opts.Page, opts.Size)))
|
|
|
|
switch opts.Sort {
|
|
case enum.PathAttrCreated:
|
|
// NOTE: string concatination is safe because the
|
|
// order attribute is an enum and is not user-defined,
|
|
// and is therefore not subject to injection attacks.
|
|
stmt = stmt.OrderBy("path_created " + opts.Order.String())
|
|
case enum.PathAttrUpdated:
|
|
stmt = stmt.OrderBy("path_updated " + opts.Order.String())
|
|
case enum.PathAttrId:
|
|
stmt = stmt.OrderBy("path_id " + opts.Order.String())
|
|
case enum.PathAttrPath:
|
|
stmt = stmt.OrderBy("path_value" + opts.Order.String())
|
|
}
|
|
|
|
sql, _, err := stmt.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Failed to convert query to sql")
|
|
}
|
|
|
|
if err = db.SelectContext(ctx, &dst, sql); err != nil {
|
|
return nil, processSqlErrorf(err, "Customer select query failed")
|
|
}
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
// Coutn paths for a target.
|
|
func CountPaths(ctx context.Context, db *sqlx.DB, targetType enum.PathTargetType, targetId int64) (int64, error) {
|
|
var count int64
|
|
err := db.QueryRowContext(ctx, pathCount, string(targetType), fmt.Sprint(targetId)).Scan(&count)
|
|
if err != nil {
|
|
return 0, processSqlErrorf(err, "Query failed")
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// Count paths for a target as part of a transaction.
|
|
func CountPathsTx(ctx context.Context, tx *sqlx.Tx, targetType enum.PathTargetType, targetId int64) (int64, error) {
|
|
var count int64
|
|
err := tx.QueryRowContext(ctx, pathCount, string(targetType), fmt.Sprint(targetId)).Scan(&count)
|
|
if err != nil {
|
|
return 0, processSqlErrorf(err, "Query failed")
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
const pathBase = `
|
|
SELECT
|
|
path_id
|
|
,path_value
|
|
,path_isAlias
|
|
,path_targetType
|
|
,path_targetId
|
|
,path_createdBy
|
|
,path_created
|
|
,path_updated
|
|
FROM paths
|
|
`
|
|
const pathSelect = pathBase + `
|
|
WHERE path_targetType = $1 AND path_targetId = $2
|
|
ORDER BY path_isAlias DESC, path_value ASC
|
|
LIMIT $3 OFFSET $4
|
|
`
|
|
|
|
// there's only one entry with a given target & targetId for isAlias -- false
|
|
const pathSelectPrimaryForTarget = pathBase + `
|
|
WHERE path_targetType = $1 AND path_targetId = $2 AND path_isAlias = 0
|
|
`
|
|
|
|
const pathSelectPrimaryForPrefix = pathBase + `
|
|
WHERE path_value LIKE $1 AND path_isAlias = 0
|
|
`
|
|
|
|
const pathCount = `
|
|
SELECT count(*)
|
|
FROM paths
|
|
WHERE path_targetType = $1 AND path_targetId = $2
|
|
`
|
|
|
|
const pathCountPrimaryForPrefix = `
|
|
SELECT count(*)
|
|
FROM paths
|
|
WHERE path_value LIKE $1 AND path_isAlias = 0
|
|
`
|
|
|
|
const pathInsert = `
|
|
INSERT INTO paths (
|
|
path_value
|
|
,path_isAlias
|
|
,path_targetType
|
|
,path_targetId
|
|
,path_createdBy
|
|
,path_created
|
|
,path_updated
|
|
) values (
|
|
:path_value
|
|
,:path_isAlias
|
|
,:path_targetType
|
|
,:path_targetId
|
|
,:path_createdBy
|
|
,:path_created
|
|
,:path_updated
|
|
) RETURNING path_id
|
|
`
|
|
|
|
const pathSelectId = pathBase + `
|
|
WHERE path_id = $1
|
|
`
|
|
|
|
const pathDeleteId = `
|
|
DELETE FROM paths
|
|
WHERE path_id = $1
|
|
`
|
|
|
|
const pathDeleteTarget = `
|
|
DELETE FROM paths
|
|
WHERE path_targetType = $1 AND path_targetId = $2
|
|
`
|
|
|
|
const pathMakeAlias = `
|
|
UPDATE paths
|
|
SET
|
|
path_isAlias = 1
|
|
WHERE path_id = :path_id
|
|
`
|