| 1 |
|
package db |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"database/sql" |
| 6 |
|
"errors" |
| 7 |
|
"fmt" |
| 8 |
|
"time" |
| 9 |
|
|
| 10 |
|
"github.com/lib/pq" |
| 11 |
|
|
| 12 |
|
"sourcecraft.dev/bigbes/sr-ht-spec/core" |
| 13 |
|
) |
| 14 |
|
|
| 15 |
|
// Space is one bare git repo: the unit of ownership, ACL and review policy. |
| 16 |
|
// The repo on disk is the real thing; this row exists so spaces can be listed |
| 17 |
|
// and so index bookkeeping has something to hang off. Ref.Owner never carries |
| 18 |
|
// the leading '~'. |
| 19 |
|
type Space struct { |
| 20 |
|
ID int |
| 21 |
|
Ref core.SpaceRef |
| 22 |
|
Created time.Time |
| 23 |
|
} |
| 24 |
|
|
| 25 |
|
const spaceSelect = `SELECT id, owner, name, created FROM space` |
| 26 |
|
|
| 27 |
13 |
func scanSpace(sc rowScanner) (*Space, error) { |
| 28 |
13 |
var sp Space |
| 29 |
13 |
if err := sc.Scan(&sp.ID, &sp.Ref.Owner, &sp.Ref.Name, &sp.Created); err != nil { |
| 30 |
2 |
return nil, err |
| 31 |
2 |
} |
| 32 |
11 |
return &sp, nil |
| 33 |
|
} |
| 34 |
|
|
| 35 |
|
// CreateSpace inserts a space row. The name is validated with core before it |
| 36 |
|
// reaches SQL: an owner or space name that is not a safe path segment would |
| 37 |
|
// become a directory under the repos root, so it is rejected at every door |
| 38 |
|
// rather than at whichever one happens to check. |
| 39 |
|
// |
| 40 |
|
// A uq_space_owner_name violation maps to ErrSpaceExists. |
| 41 |
37 |
func (s *Store) CreateSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) { |
| 42 |
37 |
if err := core.ValidateOwner(ref.Owner); err != nil { |
| 43 |
0 |
return nil, err |
| 44 |
0 |
} |
| 45 |
37 |
if err := core.ValidateSpaceName(ref.Name); err != nil { |
| 46 |
1 |
return nil, err |
| 47 |
1 |
} |
| 48 |
36 |
const q = ` |
| 49 |
36 |
INSERT INTO space (owner, name, created) |
| 50 |
36 |
VALUES ($1, $2, $3) |
| 51 |
36 |
RETURNING id, created` |
| 52 |
36 |
var sp Space |
| 53 |
36 |
sp.Ref = ref |
| 54 |
36 |
err := s.q.QueryRowContext(ctx, q, ref.Owner, ref.Name, time.Now().UTC()). |
| 55 |
36 |
Scan(&sp.ID, &sp.Created) |
| 56 |
36 |
if err != nil { |
| 57 |
1 |
var pqErr *pq.Error |
| 58 |
1 |
if errors.As(err, &pqErr) && pqErr.Code == "23505" { |
| 59 |
1 |
return nil, fmt.Errorf("%w: %s", ErrSpaceExists, ref) |
| 60 |
1 |
} |
| 61 |
0 |
return nil, fmt.Errorf("insert space %s: %w", ref, err) |
| 62 |
|
} |
| 63 |
35 |
return &sp, nil |
| 64 |
|
} |
| 65 |
|
|
| 66 |
|
// GetSpace resolves a space by owner and name. Returns ErrNotFound if no such |
| 67 |
|
// space exists. |
| 68 |
2 |
func (s *Store) GetSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) { |
| 69 |
2 |
q := spaceSelect + ` WHERE owner = $1 AND name = $2` |
| 70 |
2 |
sp, err := scanSpace(s.q.QueryRowContext(ctx, q, ref.Owner, ref.Name)) |
| 71 |
2 |
if errors.Is(err, sql.ErrNoRows) { |
| 72 |
1 |
return nil, ErrNotFound |
| 73 |
1 |
} |
| 74 |
1 |
if err != nil { |
| 75 |
0 |
return nil, fmt.Errorf("get space %s: %w", ref, err) |
| 76 |
0 |
} |
| 77 |
1 |
return sp, nil |
| 78 |
|
} |
| 79 |
|
|
| 80 |
|
// GetSpaceByID resolves a space by its primary key — the form every other table |
| 81 |
|
// references it by. Returns ErrNotFound if no such space exists. |
| 82 |
4 |
func (s *Store) GetSpaceByID(ctx context.Context, id int) (*Space, error) { |
| 83 |
4 |
q := spaceSelect + ` WHERE id = $1` |
| 84 |
4 |
sp, err := scanSpace(s.q.QueryRowContext(ctx, q, id)) |
| 85 |
4 |
if errors.Is(err, sql.ErrNoRows) { |
| 86 |
1 |
return nil, ErrNotFound |
| 87 |
1 |
} |
| 88 |
3 |
if err != nil { |
| 89 |
0 |
return nil, fmt.Errorf("get space %d: %w", id, err) |
| 90 |
0 |
} |
| 91 |
3 |
return sp, nil |
| 92 |
|
} |
| 93 |
|
|
| 94 |
|
// ListSpaces returns every space, ordered by owner then name. There is one human |
| 95 |
|
// on this instance and no visibility levels, so there is nothing to filter by: |
| 96 |
|
// the list is the whole corpus, which is also exactly what the meta-project (a |
| 97 |
|
// filter that excludes nothing) needs. |
| 98 |
1 |
func (s *Store) ListSpaces(ctx context.Context) ([]*Space, error) { |
| 99 |
1 |
q := spaceSelect + ` ORDER BY owner, name` |
| 100 |
1 |
rows, err := s.q.QueryContext(ctx, q) |
| 101 |
1 |
if err != nil { |
| 102 |
0 |
return nil, fmt.Errorf("list spaces: %w", err) |
| 103 |
0 |
} |
| 104 |
1 |
defer rows.Close() |
| 105 |
1 |
var spaces []*Space |
| 106 |
3 |
for rows.Next() { |
| 107 |
3 |
sp, err := scanSpace(rows) |
| 108 |
3 |
if err != nil { |
| 109 |
0 |
return nil, fmt.Errorf("scan space: %w", err) |
| 110 |
0 |
} |
| 111 |
3 |
spaces = append(spaces, sp) |
| 112 |
|
} |
| 113 |
1 |
if err := rows.Err(); err != nil { |
| 114 |
0 |
return nil, fmt.Errorf("iterate spaces: %w", err) |
| 115 |
0 |
} |
| 116 |
1 |
return spaces, nil |
| 117 |
|
} |