Implement list projects

Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
This commit is contained in:
2026-05-22 11:23:04 +02:00
parent 8b203e5bad
commit 475d5a5b03
5 changed files with 172 additions and 18 deletions

View File

@@ -69,6 +69,14 @@ tasks:
deps:
- migrate
down-migrations:
desc: Roll back all migrations
vars:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} down -all"
deps:
- migrate
drop-migrations:
desc: Drop migrations
vars:

View File

@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/jackc/pgerrcode"
@@ -54,6 +53,7 @@ func CreateProject(ctx context.Context, db *sql.DB, data *ProjectData) error {
return err
}
// When a project is created, we need to insert the default owner project membership
queryMembership := `
INSERT INTO project_membership
(project_uuid, user_uuid, role, status, invited_by, joined_at)
@@ -104,7 +104,6 @@ func GetProjectByID(ctx context.Context, db *sql.DB, projectID string) (*Project
}
return nil, err
}
fmt.Println(data)
return data, nil
}
@@ -125,12 +124,46 @@ func UpdateProject(ctx context.Context, db *sql.DB, data *ProjectData) error {
}
// ListProjects get all projects that are available for the user from the database
func ListProjects(ctx context.Context, db *sql.DB) ([]*ProjectData, error) {
func ListProjects(ctx context.Context, db *sql.DB, userID string) ([]*ProjectData, error) {
query := `
SELECT p.uuid, p.name
FROM projects p
JOIN project_membership pm ON pm.project_id = p.id
WHERE pm.user_id = ?`
fmt.Println(query)
return nil, nil
JOIN project_membership pm ON pm.project_uuid = p.uuid
WHERE pm.user_uuid = $1`
rows, err := db.QueryContext(ctx, query, userID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
result := []*ProjectData{}
for rows.Next() {
pd := &ProjectData{}
err := rows.Scan(&pd.UUID, &pd.Name)
if err != nil {
return nil, err
}
result = append(result, pd)
}
return result, nil
}
// GetProjectOwner should return an owner if a project
func GetProjectOwner(ctx context.Context, db *sql.DB, projectID string) (userID string, err error) {
query := `
SELECT user_uuid
FROM project_membership
WHERE project_uuid = $1 AND role = 'owner'`
if err := db.QueryRowContext(ctx, query, projectID).Scan(
&userID,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", ErrNotFound
}
return "", err
}
return
}

View File

@@ -1,7 +1,9 @@
package repository_test
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
@@ -10,8 +12,35 @@ import (
"github.com/stretchr/testify/assert"
)
func newTestUniqueEmail(prefix string) string {
if prefix == "" {
prefix = "test"
}
return fmt.Sprintf(
"%s-%d-%s@example.com",
prefix,
time.Now().UnixMilli(),
uuid.NewString(),
)
}
func accountForProject(ctx context.Context) (string, error) {
account := &repository.AccountData{
UUID: uuid.NewString(),
Email: newTestUniqueEmail("projects"),
PasswordHash: "dummy",
Name: "John",
Surname: "Doe",
}
if err := repository.CreateAccount(ctx, newTestDBConnection(ctx), account); err != nil {
return "", err
}
return account.UUID, nil
}
func TestIntegrationProjectsCreate_Success(t *testing.T) {
createdBy := uuid.NewString()
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-1",
@@ -29,7 +58,8 @@ func TestIntegrationProjectsCreate_Success(t *testing.T) {
}
func TestIntegrationProjectsCreate_CheckFailed(t *testing.T) {
createdBy := uuid.NewString()
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-2",
@@ -46,7 +76,8 @@ func TestIntegrationProjectsCreate_CheckFailed(t *testing.T) {
}
func TestIntegrationProjectsCreate_AlreadyExistsFail(t *testing.T) {
createdBy := uuid.NewString()
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-3",
@@ -65,7 +96,8 @@ func TestIntegrationProjectsCreate_AlreadyExistsFail(t *testing.T) {
}
func TestIntegrationProjectsCreateAndGet_Success(t *testing.T) {
createdBy := uuid.NewString()
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-4",
@@ -96,7 +128,8 @@ func TestIntegrationProjectsCreateAndGet_NotFound(t *testing.T) {
}
func TestIntegrationProjectsCreateUpdateAndGet_Success(t *testing.T) {
createdBy := uuid.NewString()
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-5",
@@ -131,3 +164,83 @@ func TestIntegrationProjectsCreateUpdateAndGet_Success(t *testing.T) {
assert.Equal(t, project.Slug, data.Slug)
assert.Equal(t, project.Description, data.Description)
}
func TestIntegrationProjectsCreateAndGetOwner_Success(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-6",
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))
userID, err := repository.GetProjectOwner(t.Context(), newTestDBConnection(t.Context()), project.UUID)
assert.NoError(t, err)
assert.Equal(t, project.CreatedBy, userID)
}
func TestIntegrationListProjectsWorkflow(t *testing.T) {
createdBy1, err := accountForProject(t.Context())
assert.NoError(t, err)
project1 := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "List 1",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy1,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy1,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project1))
project2 := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "List 2",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy1,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy1,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project2))
createdBy2, err := accountForProject(t.Context())
assert.NoError(t, err)
project3 := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "List 3",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy2,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy2,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project3))
projects, err := repository.ListProjects(t.Context(), newTestDBConnection(t.Context()), createdBy1)
assert.NoError(t, err)
assert.Len(t, projects, 2)
projects, err = repository.ListProjects(t.Context(), newTestDBConnection(t.Context()), createdBy2)
assert.NoError(t, err)
assert.Len(t, projects, 1)
}

View File

@@ -1 +1 @@
DROP TABLE IF EXIST prohects;
DROP TABLE projects;

View File

@@ -1,7 +1,7 @@
DROP INDEX IF EXISTS idx_project_membership_project;
DROP INDEX IF EXISTS idx_project_membership_user;
DROP INDEX idx_project_membership_project;
DROP INDEX idx_project_membership_user;
DROP TABLE IF EXISTS project_membership;
DROP TABLE project_membership;
DROP TYPE IF EXISTS membership_status;
DROP TYPE IF EXISTS project_role;
DROP TYPE membership_status CASCADE;
DROP TYPE project_role CASCADE;