Add Projects #13
27
Taskfile.yml
27
Taskfile.yml
@@ -61,6 +61,22 @@ tasks:
|
||||
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
|
||||
cmd: go run main.go migrate --migrations-path=file://migrations
|
||||
|
||||
force-migration:
|
||||
desc: Force migrate to a desired version
|
||||
vars:
|
||||
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
|
||||
cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} force {{ .CLI_ARGS }}"
|
||||
deps:
|
||||
- migrate
|
||||
|
||||
drop-migrations:
|
||||
desc: Drop migrations
|
||||
vars:
|
||||
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
|
||||
cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} drop"
|
||||
deps:
|
||||
- migrate
|
||||
|
||||
run-server-dev:
|
||||
desc: Run the local dev server
|
||||
deps:
|
||||
@@ -113,6 +129,8 @@ tasks:
|
||||
desc: Add a new database migration
|
||||
silent: true
|
||||
cmd: "{{.MIGRATE}} create -dir migrations -ext sql {{.CLI_ARGS}}"
|
||||
deps:
|
||||
- migrate
|
||||
|
||||
# Install required tools
|
||||
localbin:
|
||||
@@ -132,6 +150,7 @@ tasks:
|
||||
TARGET: "{{.MIGRATE}}"
|
||||
PACKAGE: github.com/golang-migrate/migrate/v4/cmd/migrate
|
||||
VERSION: latest
|
||||
TAGS: "postgres"
|
||||
|
||||
go-install-tool:
|
||||
internal: true
|
||||
@@ -150,7 +169,13 @@ tasks:
|
||||
echo "Downloading $PACKAGE"
|
||||
rm -f "$TARGET"
|
||||
|
||||
GOBIN="{{.LOCALBIN}}" go install "$PACKAGE"
|
||||
TAGS="{{.TAGS}}"
|
||||
if [ -n "$TAGS" ]; then
|
||||
echo "Using build tags: $TAGS"
|
||||
GOBIN="{{.LOCALBIN}}" go install -tags "$TAGS" "$PACKAGE"
|
||||
else
|
||||
GOBIN="{{.LOCALBIN}}" go install "$PACKAGE"
|
||||
fi
|
||||
|
||||
mv "{{.LOCALBIN}}/$(basename "$TARGET")" "$VERSIONED"
|
||||
ln -sf "$(realpath "$VERSIONED")" "$TARGET"
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlreadyExists = errors.New("entry already exists")
|
||||
ErrNotFound = errors.New("entry not found")
|
||||
ErrAlreadyExists = errors.New("entry already exists")
|
||||
ErrCheckNotPassed = errors.New("sql checks not passed")
|
||||
ErrNotFound = errors.New("entry not found")
|
||||
)
|
||||
|
||||
type AccountData struct {
|
||||
|
||||
@@ -3,7 +3,11 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type ProjectData struct {
|
||||
@@ -13,7 +17,7 @@ type ProjectData struct {
|
||||
Description string
|
||||
CreatedBy string
|
||||
CreatedAt time.Time
|
||||
ArchivedAt sql.NullTime
|
||||
ClosedAt sql.NullTime
|
||||
Blocked bool
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy string
|
||||
@@ -23,18 +27,67 @@ type ProjectData struct {
|
||||
func CreateProject(ctx context.Context, db *sql.DB, data *ProjectData) error {
|
||||
query := `
|
||||
INSERT INTO projects
|
||||
(uuid, name, slug, )
|
||||
(uuid, name, slug, description, owner_user_id, billing_account_id, created_by, created_at, updated_by, updated_at)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`
|
||||
|
||||
if _, err := db.ExecContext(ctx, query,
|
||||
data.UUID, data.Name, data.Slug, data.Description, data.CreatedBy,
|
||||
data.CreatedBy, data.CreatedBy, data.CreatedAt, data.CreatedBy, data.CreatedAt); err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
switch pgErr.Code {
|
||||
case pgerrcode.UniqueViolation:
|
||||
return ErrAlreadyExists
|
||||
case pgerrcode.CheckViolation:
|
||||
return ErrCheckNotPassed
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProjectByID returns a project from the database
|
||||
func GetProjectByID(ctx context.Context, db *sql.DB, projectID string) (data *ProjectData, err error) {
|
||||
return nil, nil
|
||||
func GetProjectByID(ctx context.Context, db *sql.DB, projectID string) (*ProjectData, error) {
|
||||
data := &ProjectData{}
|
||||
query := `
|
||||
SELECT uuid, name, slug, description, owner_user_id, closed_at, created_at
|
||||
FROM projects
|
||||
WHERE uuid=$1`
|
||||
|
||||
if err := db.QueryRowContext(ctx, query, projectID).Scan(
|
||||
&data.UUID,
|
||||
&data.Name,
|
||||
&data.Slug,
|
||||
&data.Description,
|
||||
&data.CreatedBy,
|
||||
&data.ClosedAt,
|
||||
&data.CreatedAt,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// UpdateProject change editable project data
|
||||
func UpdateProject(ctx context.Context, db *sql.DB, data *ProjectData) error {
|
||||
query := `
|
||||
UPDATE projects
|
||||
SET
|
||||
name = $2,
|
||||
description = $3,
|
||||
updated_at = $4,
|
||||
updated_by = $5
|
||||
WHERE uuid = $1;`
|
||||
if _, err := db.Query(query, data.UUID, data.Name, data.Description, data.UpdatedAt, data.UpdatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,20 +7,127 @@ import (
|
||||
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProjectsRepository_Success(t *testing.T) {
|
||||
func TestIntegrationProjectsCreate_Success(t *testing.T) {
|
||||
createdBy := uuid.NewString()
|
||||
_ = &repository.ProjectData{
|
||||
project := &repository.ProjectData{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "test-1",
|
||||
Slug: "test_1",
|
||||
Slug: uuid.NewString(),
|
||||
Description: "Test Project Number 1",
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
ArchivedAt: sql.NullTime{Time: time.Now()},
|
||||
ClosedAt: sql.NullTime{Time: time.Now()},
|
||||
Blocked: false,
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedBy: createdBy,
|
||||
}
|
||||
|
||||
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
|
||||
}
|
||||
|
||||
func TestIntegrationProjectsCreate_CheckFailed(t *testing.T) {
|
||||
createdBy := uuid.NewString()
|
||||
project := &repository.ProjectData{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "test-2",
|
||||
Slug: "test_2",
|
||||
Description: "Test Project Number 1",
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
ClosedAt: sql.NullTime{Time: time.Now()},
|
||||
Blocked: false,
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedBy: createdBy,
|
||||
}
|
||||
assert.Error(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project), repository.ErrCheckNotPassed)
|
||||
}
|
||||
|
||||
func TestIntegrationProjectsCreate_AlreadyExistsFail(t *testing.T) {
|
||||
createdBy := uuid.NewString()
|
||||
project := &repository.ProjectData{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "test-3",
|
||||
Slug: uuid.NewString(),
|
||||
Description: "Test Project Number 1",
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
ClosedAt: sql.NullTime{Time: time.Now()},
|
||||
Blocked: false,
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedBy: createdBy,
|
||||
}
|
||||
|
||||
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
|
||||
assert.Error(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project), repository.ErrAlreadyExists)
|
||||
}
|
||||
|
||||
func TestIntegrationProjectsCreateAndGet_Success(t *testing.T) {
|
||||
createdBy := uuid.NewString()
|
||||
project := &repository.ProjectData{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "test-4",
|
||||
Slug: uuid.NewString(),
|
||||
Description: "Test Project Number 1",
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
Blocked: false,
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedBy: createdBy,
|
||||
}
|
||||
|
||||
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
|
||||
data, err := repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), project.UUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, project.Name, data.Name)
|
||||
assert.Equal(t, project.Slug, data.Slug)
|
||||
assert.Equal(t, project.Description, data.Description)
|
||||
assert.Equal(t, project.CreatedBy, data.CreatedBy)
|
||||
assert.Equal(t, project.ClosedAt.Time.Truncate(time.Second), data.ClosedAt.Time.Truncate(time.Second))
|
||||
assert.Equal(t, project.CreatedAt.Truncate(time.Second), data.CreatedAt.Truncate(time.Second))
|
||||
}
|
||||
|
||||
func TestIntegrationProjectsCreateAndGet_NotFound(t *testing.T) {
|
||||
data, err := repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), uuid.NewString())
|
||||
assert.ErrorIs(t, err, repository.ErrNotFound)
|
||||
assert.Nil(t, data)
|
||||
}
|
||||
|
||||
func TestIntegrationProjectsCreateUpdateAndGet_Success(t *testing.T) {
|
||||
createdBy := uuid.NewString()
|
||||
project := &repository.ProjectData{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "test-5",
|
||||
Slug: uuid.NewString(),
|
||||
Description: "Test Project Number 1",
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
Blocked: false,
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedBy: createdBy,
|
||||
}
|
||||
|
||||
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
|
||||
data, err := repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), project.UUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, project.Name, data.Name)
|
||||
assert.Equal(t, project.Slug, data.Slug)
|
||||
assert.Equal(t, project.Description, data.Description)
|
||||
assert.Equal(t, project.CreatedBy, data.CreatedBy)
|
||||
assert.Equal(t, project.ClosedAt.Time.Truncate(time.Second), data.ClosedAt.Time.Truncate(time.Second))
|
||||
assert.Equal(t, project.CreatedAt.Truncate(time.Second), data.CreatedAt.Truncate(time.Second))
|
||||
|
||||
project.UpdatedBy = uuid.NewString()
|
||||
project.UpdatedAt = time.Now()
|
||||
project.Description = "Updated description"
|
||||
project.Name = "update name"
|
||||
|
||||
assert.NoError(t, repository.UpdateProject(t.Context(), newTestDBConnection(t.Context()), project))
|
||||
data, err = repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), project.UUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, project.Name, data.Name)
|
||||
assert.Equal(t, project.Slug, data.Slug)
|
||||
assert.Equal(t, project.Description, data.Description)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE projects (
|
||||
id UUID PRIMARY KEY,
|
||||
uuid UUID PRIMARY KEY,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE
|
||||
CHECK (
|
||||
@@ -7,11 +7,10 @@ CREATE TABLE projects (
|
||||
),
|
||||
description TEXT,
|
||||
owner_user_id UUID NOT NULL,
|
||||
archived_at TIMESTAMP NULL,
|
||||
closed_at TIMESTAMP NULL,
|
||||
closed_at TIMESTAMPTZ NULL,
|
||||
billing_account_id UUID NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by UUID NOT NULL,
|
||||
updated_by UUID NOT NULL,
|
||||
updated_by UUID NOT NULL
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user