package store import ( "context" "fmt" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5" "git.warky.dev/wdevs/amcs/internal/generatedmodels" thoughttypes "git.warky.dev/wdevs/amcs/internal/types" ) func (db *DB) CreateProject(ctx context.Context, name, description string) (thoughttypes.Project, error) { row := db.pool.QueryRow(ctx, ` insert into projects (name, description) values ($1, $2) returning guid, name, description, created_at, last_active_at `, name, description) var model generatedmodels.ModelPublicProjects if err := row.Scan(&model.GUID, &model.Name, &model.Description, &model.CreatedAt, &model.LastActiveAt); err != nil { return thoughttypes.Project{}, fmt.Errorf("create project: %w", err) } return projectFromModel(model), nil } func (db *DB) GetProject(ctx context.Context, nameOrID string) (thoughttypes.Project, error) { lookup := strings.TrimSpace(nameOrID) // Prefer guid lookup when input parses as UUID, but fall back to name lookup // so UUID-shaped project names can still be resolved by name. if parsedID, err := uuid.Parse(lookup); err == nil { project, queryErr := db.getProjectByGUID(ctx, parsedID) if queryErr == nil { return project, nil } if queryErr != pgx.ErrNoRows { return thoughttypes.Project{}, queryErr } } return db.getProjectByName(ctx, lookup) } func (db *DB) getProjectByGUID(ctx context.Context, id uuid.UUID) (thoughttypes.Project, error) { row := db.pool.QueryRow(ctx, ` select guid, name, description, created_at, last_active_at from projects where guid = $1 `, id) return scanProject(row) } func (db *DB) getProjectByName(ctx context.Context, name string) (thoughttypes.Project, error) { row := db.pool.QueryRow(ctx, ` select guid, name, description, created_at, last_active_at from projects where name = $1 `, name) return scanProject(row) } func scanProject(row pgx.Row) (thoughttypes.Project, error) { var model generatedmodels.ModelPublicProjects if err := row.Scan(&model.GUID, &model.Name, &model.Description, &model.CreatedAt, &model.LastActiveAt); err != nil { if err == pgx.ErrNoRows { return thoughttypes.Project{}, err } return thoughttypes.Project{}, fmt.Errorf("get project: %w", err) } return projectFromModel(model), nil } func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary, error) { rows, err := db.pool.Query(ctx, ` select p.guid, p.name, p.description, p.created_at, p.last_active_at, count(t.id) as thought_count from projects p left join thoughts t on t.project_id = p.guid and t.archived_at is null group by p.guid, p.name, p.description, p.created_at, p.last_active_at order by p.last_active_at desc, p.created_at desc `) if err != nil { return nil, fmt.Errorf("list projects: %w", err) } defer rows.Close() projects := make([]thoughttypes.ProjectSummary, 0) for rows.Next() { var model generatedmodels.ModelPublicProjects var thoughtCount int if err := rows.Scan(&model.GUID, &model.Name, &model.Description, &model.CreatedAt, &model.LastActiveAt, &thoughtCount); err != nil { return nil, fmt.Errorf("scan project summary: %w", err) } projects = append(projects, thoughttypes.ProjectSummary{ Project: projectFromModel(model), ThoughtCount: thoughtCount, }) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate projects: %w", err) } return projects, nil } func (db *DB) TouchProject(ctx context.Context, id uuid.UUID) error { tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where guid = $1`, id) if err != nil { return fmt.Errorf("touch project: %w", err) } if tag.RowsAffected() == 0 { return pgx.ErrNoRows } return nil }