Skip to content

Commit

Permalink
implement environments as a resource
Browse files Browse the repository at this point in the history
  • Loading branch information
mathnogueira committed Apr 20, 2023
1 parent 9a1b66f commit cb9f7e8
Show file tree
Hide file tree
Showing 8 changed files with 550 additions and 27 deletions.
342 changes: 342 additions & 0 deletions server/environment/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
package environment

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/kubeshop/tracetest/server/id"
"github.com/kubeshop/tracetest/server/resourcemanager"
)

type Repository interface {
SortingFields() []string
SetID(environment Environment, id id.ID) Environment
Create(ctx context.Context, environment Environment) (Environment, error)
Update(ctx context.Context, environment Environment) (Environment, error)
Delete(ctx context.Context, id id.ID) error
List(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]Environment, error)
Count(ctx context.Context, query string) (int, error)
Get(ctx context.Context, id id.ID) (Environment, error)
}

type repository struct {
db *sql.DB
}

func NewRepository(db *sql.DB) Repository {
return &repository{db}
}

type scanner interface {
Scan(dest ...interface{}) error
}

const (
insertQuery = `
INSERT INTO environments (
"id",
"name",
"description",
"created_at",
"values"
) VALUES ($1, $2, $3, $4, $5)`

updateQuery = `
UPDATE environments SET
"id" = $2,
"name" = $3,
"description" = $4,
"created_at" = $5,
"values" = $6
WHERE id = $1
`

getQuery = `
SELECT
e.id,
e.name,
e.description,
e.created_at,
e.values
FROM environments e
`

deleteQuery = "DELETE FROM environments WHERE id = $1"

idExistsQuery = `
SELECT COUNT(*) > 0 as exists FROM environments WHERE id = $1
`

countQuery = `
SELECT COUNT(*) FROM environments e
`
)

var _ resourcemanager.Create[Environment] = &repository{}
var _ resourcemanager.Delete[Environment] = &repository{}
var _ resourcemanager.Get[Environment] = &repository{}
var _ resourcemanager.List[Environment] = &repository{}
var _ resourcemanager.Update[Environment] = &repository{}
var _ resourcemanager.IDSetter[Environment] = &repository{}
var _ resourcemanager.Provision[Environment] = &repository{}

func (*repository) SortingFields() []string {
return []string{"name", "createdAt"}
}

func (r *repository) SetID(environment Environment, id id.ID) Environment {
environment.ID = id
return environment
}

func (r *repository) Create(ctx context.Context, environment Environment) (Environment, error) {
environment.ID = environment.Slug()
environment.CreatedAt = time.Now().UTC().Format(time.RFC3339Nano)

return r.insertIntoEnvironments(ctx, environment)
}

func (r *repository) Update(ctx context.Context, environment Environment) (Environment, error) {
oldEnvironment, err := r.Get(ctx, environment.ID)
if err != nil {
return Environment{}, err
}

// keep the same creation date to keep sort order
environment.CreatedAt = oldEnvironment.CreatedAt
environment.ID = oldEnvironment.ID

return r.updateIntoEnvironments(ctx, environment, oldEnvironment.ID)
}

func (r *repository) Delete(ctx context.Context, id id.ID) error {
_, err := r.Get(ctx, id)
if err != nil {
return err
}

tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("sql BeginTx: %w", err)
}

_, err = tx.ExecContext(ctx, deleteQuery, id)

if err != nil {
tx.Rollback()
return fmt.Errorf("sql error: %w", err)
}

err = tx.Commit()
if err != nil {
return fmt.Errorf("sql Commit: %w", err)
}

return nil
}

func (r *repository) List(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]Environment, error) {
if take == 0 {
take = 20
}

params := []any{take, skip}

sql := getQuery

const condition = "WHERE (e.name ilike $3 OR e.description ilike $3) "
if query != "" {
sql += condition
params = append(params, query)
}

sortingFields := map[string]string{
"created": "e.created_at",
"name": "e.name",
}

sql = sortQuery(sql, sortBy, sortDirection, sortingFields)
sql += ` LIMIT $1 OFFSET $2 `

stmt, err := r.db.Prepare(sql)
if err != nil {
return []Environment{}, err
}
defer stmt.Close()

rows, err := stmt.QueryContext(ctx, params...)
if err != nil {
return []Environment{}, err
}

environments := []Environment{}

for rows.Next() {
environment, err := r.readEnvironmentRow(ctx, rows)
if err != nil {
return []Environment{}, err
}

environments = append(environments, environment)
}

return environments, nil
}

func (r *repository) Count(ctx context.Context, query string) (int, error) {
return r.countEnvironments(ctx, query)
}

func sortQuery(sql, sortBy, sortDirection string, sortingFields map[string]string) string {
sortField, ok := sortingFields[sortBy]

if !ok {
sortField = sortingFields["created"]
}

dir := "DESC"
if strings.ToLower(sortDirection) == "asc" {
dir = "ASC"
}

return fmt.Sprintf("%s ORDER BY %s %s", sql, sortField, dir)
}

func (r *repository) Get(ctx context.Context, id id.ID) (Environment, error) {
stmt, err := r.db.Prepare(getQuery + " WHERE e.id = $1")

if err != nil {
return Environment{}, fmt.Errorf("prepare: %w", err)
}
defer stmt.Close()

environment, err := r.readEnvironmentRow(ctx, stmt.QueryRowContext(ctx, id))
if err != nil {
return Environment{}, err
}

return environment, nil
}

func (r *repository) EnvironmentIDExists(ctx context.Context, id string) (bool, error) {
exists := false

row := r.db.QueryRowContext(ctx, idExistsQuery, id)

err := row.Scan(&exists)

return exists, err
}

func (r *repository) readEnvironmentRow(ctx context.Context, row scanner) (Environment, error) {
environment := Environment{}

var (
jsonValues []byte
)
err := row.Scan(
&environment.ID,
&environment.Name,
&environment.Description,
&environment.CreatedAt,
&jsonValues,
)

switch err {
case sql.ErrNoRows:
return Environment{}, err
case nil:
err = json.Unmarshal(jsonValues, &environment.Values)
if err != nil {
return Environment{}, fmt.Errorf("cannot parse environment: %w", err)
}

return environment, nil
default:
return Environment{}, err
}
}

func (r *repository) countEnvironments(ctx context.Context, query string) (int, error) {
var (
count int
params []any
)

sql := countQuery + query

err := r.db.
QueryRowContext(ctx, sql, params...).
Scan(&count)

if err != nil {
return 0, err
}
return count, nil
}

func (r *repository) insertIntoEnvironments(ctx context.Context, environment Environment) (Environment, error) {
stmt, err := r.db.Prepare(insertQuery)
if err != nil {
return Environment{}, fmt.Errorf("sql prepare: %w", err)
}
defer stmt.Close()

jsonValues, err := json.Marshal(environment.Values)
if err != nil {
return Environment{}, fmt.Errorf("encoding error: %w", err)
}

_, err = stmt.ExecContext(
ctx,
environment.ID,
environment.Name,
environment.Description,
environment.CreatedAt,
jsonValues,
)

if err != nil {
return Environment{}, fmt.Errorf("sql exec: %w", err)
}

return environment, nil
}

func (r *repository) updateIntoEnvironments(ctx context.Context, environment Environment, oldId id.ID) (Environment, error) {
stmt, err := r.db.Prepare(updateQuery)
if err != nil {
return Environment{}, fmt.Errorf("sql prepare: %w", err)
}
defer stmt.Close()

jsonValues, err := json.Marshal(environment.Values)
if err != nil {
return Environment{}, fmt.Errorf("encoding error: %w", err)
}

_, err = stmt.ExecContext(
ctx,
oldId,
environment.Slug(),
environment.Name,
environment.Description,
environment.CreatedAt,
jsonValues,
)

if err != nil {
return Environment{}, fmt.Errorf("sql exec: %w", err)
}

return environment, nil
}

func (r *repository) Provision(ctx context.Context, environment Environment) error {
_, err := r.Create(ctx, environment)
return err
}

0 comments on commit cb9f7e8

Please sign in to comment.