M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +9 -2
@@ 8,6 8,7 @@ import (
"database/sql"
"fmt"
"sort"
+ "strings"
"git.sr.ht/~sircmpwn/git.sr.ht/api/auth"
"git.sr.ht/~sircmpwn/git.sr.ht/api/graph/generated"
@@ 81,11 82,17 @@ func (r *queryResolver) Repository(ctx context.Context, id int) (*model.Reposito
}
func (r *queryResolver) RepositoryByName(ctx context.Context, name string) (*model.Repository, error) {
- panic(fmt.Errorf("not implemented"))
+ return loaders.ForContext(ctx).RepositoriesByName.Load(name)
}
func (r *queryResolver) RepositoryByOwner(ctx context.Context, owner string, repo string) (*model.Repository, error) {
- panic(fmt.Errorf("not implemented"))
+ if strings.HasPrefix(owner, "~") {
+ owner = owner[1:]
+ } else {
+ return nil, fmt.Errorf("Expected owner to be a canonical name")
+ }
+ return loaders.ForContext(ctx).
+ RepositoriesByOwnerRepoName.Load([2]string{owner, repo})
}
func (r *repositoryResolver) Owner(ctx context.Context, obj *model.Repository) (model.Entity, error) {
M api/loaders/middleware.go => api/loaders/middleware.go +110 -4
@@ 1,8 1,10 @@
package loaders
//go:generate ./gen RepositoriesByIDLoader int api/graph/model.Repository
-//go:generate ./gen UsersByNameLoader string api/graph/model.User
+//go:generate ./gen RepositoriesByNameLoader string api/graph/model.Repository
+//go:generate ./gen RepositoriesByOwnerRepoNameLoader [2]string api/graph/model.Repository
//go:generate ./gen UsersByIDLoader int api/graph/model.User
+//go:generate ./gen UsersByNameLoader string api/graph/model.User
import (
"context"
@@ 23,9 25,11 @@ type contextKey struct {
}
type Loaders struct {
- UsersByID UsersByIDLoader
- UsersByName UsersByNameLoader
- RepositoriesByID RepositoriesByIDLoader
+ UsersByID UsersByIDLoader
+ UsersByName UsersByNameLoader
+ RepositoriesByID RepositoriesByIDLoader
+ RepositoriesByName RepositoriesByNameLoader
+ RepositoriesByOwnerRepoName RepositoriesByOwnerRepoNameLoader
}
func fetchUsersByID(ctx context.Context,
@@ 143,6 147,98 @@ func fetchRepositoriesByID(ctx context.Context,
}
}
+func fetchRepositoriesByName(ctx context.Context,
+ db *sql.DB) func (names []string) ([]*model.Repository, []error) {
+ return func (names []string) ([]*model.Repository, []error) {
+ var (
+ err error
+ rows *sql.Rows
+ )
+ if rows, err = db.QueryContext(ctx, `
+ SELECT DISTINCT `+(&model.Repository{}).Columns(ctx, "repo")+`
+ FROM repository repo
+ WHERE repo.name = ANY($2) AND repo.owner_id = $1
+ `, auth.ForContext(ctx).ID, pq.Array(names)); err != nil {
+ panic(err)
+ }
+ defer rows.Close()
+
+ reposByName := map[string]*model.Repository{}
+ for rows.Next() {
+ repo := model.Repository{}
+ if err := rows.Scan(repo.Fields(ctx)...); err != nil {
+ panic(err)
+ }
+ reposByName[repo.Name] = &repo
+ }
+ if err = rows.Err(); err != nil {
+ panic(err)
+ }
+
+ repos := make([]*model.Repository, len(names))
+ for i, name := range names {
+ repos[i] = reposByName[name]
+ }
+
+ return repos, nil
+ }
+}
+
+func fetchRepositoriesByOwnerRepoName(ctx context.Context,
+ db *sql.DB) func (names [][2]string) ([]*model.Repository, []error) {
+ return func (names [][2]string) ([]*model.Repository, []error) {
+ var (
+ err error
+ rows *sql.Rows
+ _names []string = make([]string, len(names))
+ )
+ for i, name := range names {
+ // This is a hack, but it works around limitations with PostgreSQL
+ // and is guaranteed to work because / is invalid in both usernames
+ // and repo names
+ _names[i] = name[0] + "/" + name[1]
+ }
+ if rows, err = db.QueryContext(ctx, `
+ SELECT DISTINCT `+(&model.Repository{}).Columns(ctx, "repo")+`,
+ u.username
+ FROM repository repo
+ JOIN
+ "user" u ON repo.owner_id = u.id
+ FULL OUTER JOIN
+ access ON repo.id = access.repo_id
+ WHERE
+ u.username || '/' || repo.name = ANY($2)
+ AND (access.user_id = $1
+ OR repo.owner_id = $1
+ OR repo.visibility != 'private')
+ `, auth.ForContext(ctx).ID, pq.Array(_names)); err != nil {
+ panic(err)
+ }
+ defer rows.Close()
+
+ reposByOwnerRepoName := map[[2]string]*model.Repository{}
+ for rows.Next() {
+ var ownerName string
+ repo := model.Repository{}
+ if err := rows.Scan(append(
+ repo.Fields(ctx), &ownerName)...); err != nil {
+ panic(err)
+ }
+ reposByOwnerRepoName[[2]string{ownerName, repo.Name}] = &repo
+ }
+ if err = rows.Err(); err != nil {
+ panic(err)
+ }
+
+ repos := make([]*model.Repository, len(names))
+ for i, name := range names {
+ repos[i] = reposByOwnerRepoName[name]
+ }
+
+ return repos, nil
+ }
+}
+
func Middleware(db *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ 162,6 258,16 @@ func Middleware(db *sql.DB) func(http.Handler) http.Handler {
wait: 1 * time.Millisecond,
fetch: fetchRepositoriesByID(r.Context(), db),
},
+ RepositoriesByName: RepositoriesByNameLoader{
+ maxBatch: 100,
+ wait: 1 * time.Millisecond,
+ fetch: fetchRepositoriesByName(r.Context(), db),
+ },
+ RepositoriesByOwnerRepoName: RepositoriesByOwnerRepoNameLoader{
+ maxBatch: 100,
+ wait: 1 * time.Millisecond,
+ fetch: fetchRepositoriesByOwnerRepoName(r.Context(), db),
+ },
})
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
A api/loaders/repositoriesbynameloader_gen.go => api/loaders/repositoriesbynameloader_gen.go +224 -0
@@ 0,0 1,224 @@
+// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
+
+package loaders
+
+import (
+ "sync"
+ "time"
+
+ "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model"
+)
+
+// RepositoriesByNameLoaderConfig captures the config to create a new RepositoriesByNameLoader
+type RepositoriesByNameLoaderConfig struct {
+ // Fetch is a method that provides the data for the loader
+ Fetch func(keys []string) ([]*model.Repository, []error)
+
+ // Wait is how long wait before sending a batch
+ Wait time.Duration
+
+ // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
+ MaxBatch int
+}
+
+// NewRepositoriesByNameLoader creates a new RepositoriesByNameLoader given a fetch, wait, and maxBatch
+func NewRepositoriesByNameLoader(config RepositoriesByNameLoaderConfig) *RepositoriesByNameLoader {
+ return &RepositoriesByNameLoader{
+ fetch: config.Fetch,
+ wait: config.Wait,
+ maxBatch: config.MaxBatch,
+ }
+}
+
+// RepositoriesByNameLoader batches and caches requests
+type RepositoriesByNameLoader struct {
+ // this method provides the data for the loader
+ fetch func(keys []string) ([]*model.Repository, []error)
+
+ // how long to done before sending a batch
+ wait time.Duration
+
+ // this will limit the maximum number of keys to send in one batch, 0 = no limit
+ maxBatch int
+
+ // INTERNAL
+
+ // lazily created cache
+ cache map[string]*model.Repository
+
+ // the current batch. keys will continue to be collected until timeout is hit,
+ // then everything will be sent to the fetch method and out to the listeners
+ batch *repositoriesByNameLoaderBatch
+
+ // mutex to prevent races
+ mu sync.Mutex
+}
+
+type repositoriesByNameLoaderBatch struct {
+ keys []string
+ data []*model.Repository
+ error []error
+ closing bool
+ done chan struct{}
+}
+
+// Load a Repository by key, batching and caching will be applied automatically
+func (l *RepositoriesByNameLoader) Load(key string) (*model.Repository, error) {
+ return l.LoadThunk(key)()
+}
+
+// LoadThunk returns a function that when called will block waiting for a Repository.
+// This method should be used if you want one goroutine to make requests to many
+// different data loaders without blocking until the thunk is called.
+func (l *RepositoriesByNameLoader) LoadThunk(key string) func() (*model.Repository, error) {
+ l.mu.Lock()
+ if it, ok := l.cache[key]; ok {
+ l.mu.Unlock()
+ return func() (*model.Repository, error) {
+ return it, nil
+ }
+ }
+ if l.batch == nil {
+ l.batch = &repositoriesByNameLoaderBatch{done: make(chan struct{})}
+ }
+ batch := l.batch
+ pos := batch.keyIndex(l, key)
+ l.mu.Unlock()
+
+ return func() (*model.Repository, error) {
+ <-batch.done
+
+ var data *model.Repository
+ if pos < len(batch.data) {
+ data = batch.data[pos]
+ }
+
+ var err error
+ // its convenient to be able to return a single error for everything
+ if len(batch.error) == 1 {
+ err = batch.error[0]
+ } else if batch.error != nil {
+ err = batch.error[pos]
+ }
+
+ if err == nil {
+ l.mu.Lock()
+ l.unsafeSet(key, data)
+ l.mu.Unlock()
+ }
+
+ return data, err
+ }
+}
+
+// LoadAll fetches many keys at once. It will be broken into appropriate sized
+// sub batches depending on how the loader is configured
+func (l *RepositoriesByNameLoader) LoadAll(keys []string) ([]*model.Repository, []error) {
+ results := make([]func() (*model.Repository, error), len(keys))
+
+ for i, key := range keys {
+ results[i] = l.LoadThunk(key)
+ }
+
+ repositorys := make([]*model.Repository, len(keys))
+ errors := make([]error, len(keys))
+ for i, thunk := range results {
+ repositorys[i], errors[i] = thunk()
+ }
+ return repositorys, errors
+}
+
+// LoadAllThunk returns a function that when called will block waiting for a Repositorys.
+// This method should be used if you want one goroutine to make requests to many
+// different data loaders without blocking until the thunk is called.
+func (l *RepositoriesByNameLoader) LoadAllThunk(keys []string) func() ([]*model.Repository, []error) {
+ results := make([]func() (*model.Repository, error), len(keys))
+ for i, key := range keys {
+ results[i] = l.LoadThunk(key)
+ }
+ return func() ([]*model.Repository, []error) {
+ repositorys := make([]*model.Repository, len(keys))
+ errors := make([]error, len(keys))
+ for i, thunk := range results {
+ repositorys[i], errors[i] = thunk()
+ }
+ return repositorys, errors
+ }
+}
+
+// Prime the cache with the provided key and value. If the key already exists, no change is made
+// and false is returned.
+// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
+func (l *RepositoriesByNameLoader) Prime(key string, value *model.Repository) bool {
+ l.mu.Lock()
+ var found bool
+ if _, found = l.cache[key]; !found {
+ // make a copy when writing to the cache, its easy to pass a pointer in from a loop var
+ // and end up with the whole cache pointing to the same value.
+ cpy := *value
+ l.unsafeSet(key, &cpy)
+ }
+ l.mu.Unlock()
+ return !found
+}
+
+// Clear the value at key from the cache, if it exists
+func (l *RepositoriesByNameLoader) Clear(key string) {
+ l.mu.Lock()
+ delete(l.cache, key)
+ l.mu.Unlock()
+}
+
+func (l *RepositoriesByNameLoader) unsafeSet(key string, value *model.Repository) {
+ if l.cache == nil {
+ l.cache = map[string]*model.Repository{}
+ }
+ l.cache[key] = value
+}
+
+// keyIndex will return the location of the key in the batch, if its not found
+// it will add the key to the batch
+func (b *repositoriesByNameLoaderBatch) keyIndex(l *RepositoriesByNameLoader, key string) int {
+ for i, existingKey := range b.keys {
+ if key == existingKey {
+ return i
+ }
+ }
+
+ pos := len(b.keys)
+ b.keys = append(b.keys, key)
+ if pos == 0 {
+ go b.startTimer(l)
+ }
+
+ if l.maxBatch != 0 && pos >= l.maxBatch-1 {
+ if !b.closing {
+ b.closing = true
+ l.batch = nil
+ go b.end(l)
+ }
+ }
+
+ return pos
+}
+
+func (b *repositoriesByNameLoaderBatch) startTimer(l *RepositoriesByNameLoader) {
+ time.Sleep(l.wait)
+ l.mu.Lock()
+
+ // we must have hit a batch limit and are already finalizing this batch
+ if b.closing {
+ l.mu.Unlock()
+ return
+ }
+
+ l.batch = nil
+ l.mu.Unlock()
+
+ b.end(l)
+}
+
+func (b *repositoriesByNameLoaderBatch) end(l *RepositoriesByNameLoader) {
+ b.data, b.error = l.fetch(b.keys)
+ close(b.done)
+}
A api/loaders/repositoriesbyownerreponameloader_gen.go => api/loaders/repositoriesbyownerreponameloader_gen.go +224 -0
@@ 0,0 1,224 @@
+// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
+
+package loaders
+
+import (
+ "sync"
+ "time"
+
+ "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model"
+)
+
+// RepositoriesByOwnerRepoNameLoaderConfig captures the config to create a new RepositoriesByOwnerRepoNameLoader
+type RepositoriesByOwnerRepoNameLoaderConfig struct {
+ // Fetch is a method that provides the data for the loader
+ Fetch func(keys [][2]string) ([]*model.Repository, []error)
+
+ // Wait is how long wait before sending a batch
+ Wait time.Duration
+
+ // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
+ MaxBatch int
+}
+
+// NewRepositoriesByOwnerRepoNameLoader creates a new RepositoriesByOwnerRepoNameLoader given a fetch, wait, and maxBatch
+func NewRepositoriesByOwnerRepoNameLoader(config RepositoriesByOwnerRepoNameLoaderConfig) *RepositoriesByOwnerRepoNameLoader {
+ return &RepositoriesByOwnerRepoNameLoader{
+ fetch: config.Fetch,
+ wait: config.Wait,
+ maxBatch: config.MaxBatch,
+ }
+}
+
+// RepositoriesByOwnerRepoNameLoader batches and caches requests
+type RepositoriesByOwnerRepoNameLoader struct {
+ // this method provides the data for the loader
+ fetch func(keys [][2]string) ([]*model.Repository, []error)
+
+ // how long to done before sending a batch
+ wait time.Duration
+
+ // this will limit the maximum number of keys to send in one batch, 0 = no limit
+ maxBatch int
+
+ // INTERNAL
+
+ // lazily created cache
+ cache map[[2]string]*model.Repository
+
+ // the current batch. keys will continue to be collected until timeout is hit,
+ // then everything will be sent to the fetch method and out to the listeners
+ batch *repositoriesByOwnerRepoNameLoaderBatch
+
+ // mutex to prevent races
+ mu sync.Mutex
+}
+
+type repositoriesByOwnerRepoNameLoaderBatch struct {
+ keys [][2]string
+ data []*model.Repository
+ error []error
+ closing bool
+ done chan struct{}
+}
+
+// Load a Repository by key, batching and caching will be applied automatically
+func (l *RepositoriesByOwnerRepoNameLoader) Load(key [2]string) (*model.Repository, error) {
+ return l.LoadThunk(key)()
+}
+
+// LoadThunk returns a function that when called will block waiting for a Repository.
+// This method should be used if you want one goroutine to make requests to many
+// different data loaders without blocking until the thunk is called.
+func (l *RepositoriesByOwnerRepoNameLoader) LoadThunk(key [2]string) func() (*model.Repository, error) {
+ l.mu.Lock()
+ if it, ok := l.cache[key]; ok {
+ l.mu.Unlock()
+ return func() (*model.Repository, error) {
+ return it, nil
+ }
+ }
+ if l.batch == nil {
+ l.batch = &repositoriesByOwnerRepoNameLoaderBatch{done: make(chan struct{})}
+ }
+ batch := l.batch
+ pos := batch.keyIndex(l, key)
+ l.mu.Unlock()
+
+ return func() (*model.Repository, error) {
+ <-batch.done
+
+ var data *model.Repository
+ if pos < len(batch.data) {
+ data = batch.data[pos]
+ }
+
+ var err error
+ // its convenient to be able to return a single error for everything
+ if len(batch.error) == 1 {
+ err = batch.error[0]
+ } else if batch.error != nil {
+ err = batch.error[pos]
+ }
+
+ if err == nil {
+ l.mu.Lock()
+ l.unsafeSet(key, data)
+ l.mu.Unlock()
+ }
+
+ return data, err
+ }
+}
+
+// LoadAll fetches many keys at once. It will be broken into appropriate sized
+// sub batches depending on how the loader is configured
+func (l *RepositoriesByOwnerRepoNameLoader) LoadAll(keys [][2]string) ([]*model.Repository, []error) {
+ results := make([]func() (*model.Repository, error), len(keys))
+
+ for i, key := range keys {
+ results[i] = l.LoadThunk(key)
+ }
+
+ repositorys := make([]*model.Repository, len(keys))
+ errors := make([]error, len(keys))
+ for i, thunk := range results {
+ repositorys[i], errors[i] = thunk()
+ }
+ return repositorys, errors
+}
+
+// LoadAllThunk returns a function that when called will block waiting for a Repositorys.
+// This method should be used if you want one goroutine to make requests to many
+// different data loaders without blocking until the thunk is called.
+func (l *RepositoriesByOwnerRepoNameLoader) LoadAllThunk(keys [][2]string) func() ([]*model.Repository, []error) {
+ results := make([]func() (*model.Repository, error), len(keys))
+ for i, key := range keys {
+ results[i] = l.LoadThunk(key)
+ }
+ return func() ([]*model.Repository, []error) {
+ repositorys := make([]*model.Repository, len(keys))
+ errors := make([]error, len(keys))
+ for i, thunk := range results {
+ repositorys[i], errors[i] = thunk()
+ }
+ return repositorys, errors
+ }
+}
+
+// Prime the cache with the provided key and value. If the key already exists, no change is made
+// and false is returned.
+// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
+func (l *RepositoriesByOwnerRepoNameLoader) Prime(key [2]string, value *model.Repository) bool {
+ l.mu.Lock()
+ var found bool
+ if _, found = l.cache[key]; !found {
+ // make a copy when writing to the cache, its easy to pass a pointer in from a loop var
+ // and end up with the whole cache pointing to the same value.
+ cpy := *value
+ l.unsafeSet(key, &cpy)
+ }
+ l.mu.Unlock()
+ return !found
+}
+
+// Clear the value at key from the cache, if it exists
+func (l *RepositoriesByOwnerRepoNameLoader) Clear(key [2]string) {
+ l.mu.Lock()
+ delete(l.cache, key)
+ l.mu.Unlock()
+}
+
+func (l *RepositoriesByOwnerRepoNameLoader) unsafeSet(key [2]string, value *model.Repository) {
+ if l.cache == nil {
+ l.cache = map[[2]string]*model.Repository{}
+ }
+ l.cache[key] = value
+}
+
+// keyIndex will return the location of the key in the batch, if its not found
+// it will add the key to the batch
+func (b *repositoriesByOwnerRepoNameLoaderBatch) keyIndex(l *RepositoriesByOwnerRepoNameLoader, key [2]string) int {
+ for i, existingKey := range b.keys {
+ if key == existingKey {
+ return i
+ }
+ }
+
+ pos := len(b.keys)
+ b.keys = append(b.keys, key)
+ if pos == 0 {
+ go b.startTimer(l)
+ }
+
+ if l.maxBatch != 0 && pos >= l.maxBatch-1 {
+ if !b.closing {
+ b.closing = true
+ l.batch = nil
+ go b.end(l)
+ }
+ }
+
+ return pos
+}
+
+func (b *repositoriesByOwnerRepoNameLoaderBatch) startTimer(l *RepositoriesByOwnerRepoNameLoader) {
+ time.Sleep(l.wait)
+ l.mu.Lock()
+
+ // we must have hit a batch limit and are already finalizing this batch
+ if b.closing {
+ l.mu.Unlock()
+ return
+ }
+
+ l.batch = nil
+ l.mu.Unlock()
+
+ b.end(l)
+}
+
+func (b *repositoriesByOwnerRepoNameLoaderBatch) end(l *RepositoriesByOwnerRepoNameLoader) {
+ b.data, b.error = l.fetch(b.keys)
+ close(b.done)
+}