A api/database/filter.go => api/database/filter.go +51 -0
@@ 0,0 1,51 @@
+package database
+
+import (
+ "strings"
+
+ "github.com/google/shlex"
+ sq "github.com/Masterminds/squirrel"
+)
+
+type KeyFunc func(sq.SelectBuilder, string) (string, error)
+
+type SearchTerm struct {
+ Key string
+ Value string
+ Inverse bool
+}
+
+type Searchable interface {
+ Selectable
+
+ // Update the select builder for bare search terms
+ DefaultSearch(sq.SelectBuilder, string) (sq.SelectBuilder, error)
+
+ // Return a map of KeyFuncs for each search key, whose values update the
+ // select builder for the given search term
+ //KeySearch() map[string]KeyFunc
+
+ // Update the select builder for a key/value pair which is unknown
+ //FallbackSearch(sq.SelectBuilder,
+ // key, value string) (sq.SelectBuilder, error)
+}
+
+func ApplyFilter(query sq.SelectBuilder, resource Searchable,
+ search string) (sq.SelectBuilder, error) {
+ terms, err := shlex.Split(search)
+ if err != nil {
+ return query, err
+ }
+
+ for _, term := range terms {
+ parts := strings.SplitN(term, ":", 2)
+ if len(parts) == 1 {
+ query, err = resource.DefaultSearch(query, term)
+ if err != nil {
+ return query, err
+ }
+ }
+ }
+
+ return query, nil
+}
M api/database/ql.go => api/database/ql.go +4 -2
@@ 4,6 4,7 @@ import (
"context"
"sort"
+ "github.com/lib/pq"
"github.com/vektah/gqlparser/v2/ast"
"git.sr.ht/~sircmpwn/gqlgen/graphql"
@@ 32,9 33,10 @@ func ColumnsFor(ctx context.Context, alias string,
for _, qlCol := range fields {
if sqlCol, ok := colMap[qlCol.Name]; ok {
if alias != "" {
- columns = append(columns, alias+"."+sqlCol)
+ columns = append(columns, pq.QuoteIdentifier(alias)+
+ "."+pq.QuoteIdentifier(sqlCol))
} else {
- columns = append(columns, sqlCol)
+ columns = append(columns, pq.QuoteIdentifier(sqlCol))
}
}
}
M api/graph/generated/generated.go => api/graph/generated/generated.go +21 -13
@@ 102,7 102,7 @@ type ComplexityRoot struct {
Query struct {
Me func(childComplexity int) int
- Repositories func(childComplexity int, next *int, filter *model.FilterBy) int
+ Repositories func(childComplexity int, count *int, next *int, filter *model.FilterBy) int
Repository func(childComplexity int, id int) int
RepositoryByName func(childComplexity int, name string) int
RepositoryByOwner func(childComplexity int, owner string, repo string) int
@@ 206,7 206,7 @@ type QueryResolver interface {
Version(ctx context.Context) (*model.Version, error)
Me(ctx context.Context) (*model.User, error)
User(ctx context.Context, username string) (*model.User, error)
- Repositories(ctx context.Context, next *int, filter *model.FilterBy) ([]*model.Repository, error)
+ Repositories(ctx context.Context, count *int, next *int, filter *model.FilterBy) ([]*model.Repository, error)
Repository(ctx context.Context, id int) (*model.Repository, error)
RepositoryByName(ctx context.Context, name string) (*model.Repository, error)
RepositoryByOwner(ctx context.Context, owner string, repo string) (*model.Repository, error)
@@ 533,7 533,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
- return e.complexity.Query.Repositories(childComplexity, args["next"].(*int), args["filter"].(*model.FilterBy)), true
+ return e.complexity.Query.Repositories(childComplexity, args["count"].(*int), args["next"].(*int), args["filter"].(*model.FilterBy)), true
case "Query.repository":
if e.complexity.Query.Repository == nil {
@@ 1322,7 1322,7 @@ type Tag implements Object {
# Specifies filtering criteria for a listing query
input FilterBy {
# Same search syntax as searching on the web UI
- terms: String!
+ search: String!
}
type Query {
@@ 1341,7 1341,7 @@ type Query {
# will be to return all repositories that the user either (1) has been given
# explicit access to via ACLs or (2) has implicit access to either by
# ownership or group membership.
- repositories(next: Int, filter: FilterBy): [Repository]!
+ repositories(count: Int = 10, next: Int, filter: FilterBy): [Repository]!
# Returns a specific repository
repository(id: Int!): Repository
@@ 1555,21 1555,29 @@ func (ec *executionContext) field_Query_repositories_args(ctx context.Context, r
var err error
args := map[string]interface{}{}
var arg0 *int
- if tmp, ok := rawArgs["next"]; ok {
+ if tmp, ok := rawArgs["count"]; ok {
arg0, err = ec.unmarshalOInt2ᚖint(ctx, tmp)
if err != nil {
return nil, err
}
}
- args["next"] = arg0
- var arg1 *model.FilterBy
+ args["count"] = arg0
+ var arg1 *int
+ if tmp, ok := rawArgs["next"]; ok {
+ arg1, err = ec.unmarshalOInt2ᚖint(ctx, tmp)
+ if err != nil {
+ return nil, err
+ }
+ }
+ args["next"] = arg1
+ var arg2 *model.FilterBy
if tmp, ok := rawArgs["filter"]; ok {
- arg1, err = ec.unmarshalOFilterBy2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐFilterBy(ctx, tmp)
+ arg2, err = ec.unmarshalOFilterBy2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐFilterBy(ctx, tmp)
if err != nil {
return nil, err
}
}
- args["filter"] = arg1
+ args["filter"] = arg2
return args, nil
}
@@ 3248,7 3256,7 @@ func (ec *executionContext) _Query_repositories(ctx context.Context, field graph
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
- return ec.resolvers.Query().Repositories(rctx, args["next"].(*int), args["filter"].(*model.FilterBy))
+ return ec.resolvers.Query().Repositories(rctx, args["count"].(*int), args["next"].(*int), args["filter"].(*model.FilterBy))
})
if err != nil {
ec.Error(ctx, err)
@@ 6410,9 6418,9 @@ func (ec *executionContext) unmarshalInputFilterBy(ctx context.Context, obj inte
for k, v := range asMap {
switch k {
- case "terms":
+ case "search":
var err error
- it.Terms, err = ec.unmarshalNString2string(ctx, v)
+ it.Search, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
D api/graph/model/filter.go => api/graph/model/filter.go +0 -70
@@ 1,70 0,0 @@
-package model
-
-import (
- "fmt"
- "strings"
-
- "github.com/google/shlex"
- "github.com/lib/pq"
-)
-
-type KeyFunc func(tbl, value string) (string, error)
-
-type SearchTerm struct {
- Key string
- Value string
- Inverse bool
-}
-
-type Searchable interface {
- // Returns the default WHERE clause for a given search term with no key.
- Default(tbl, term string) (string, error)
-
- // Returns a map of search functions for a given key, where each function
- // returns the appropriate WHERE clause for searching with the given
- // string.
- Keys() map[string]KeyFunc
-
- // Returns a WHERE clause for a given search key and value, where the key
- // was not found in the Keys() map.
- Fallback(tbl, key, value string) (string, error)
-}
-
-type WhereClause struct {
- Clause string
- Parameters []interface{}
-}
-
-// Returns a WHERE clause for the given fitler
-func Where(query *string, tbl string, param int,
- resource Searchable) (*WhereClause, error) {
- if query == nil {
- return &WhereClause{"true /* No search terms */", nil}, nil
- }
-
- tbl = pq.QuoteIdentifier(tbl)
- terms, err := shlex.Split(*query)
- if err != nil {
- return nil, err
- }
-
- var (
- clauses []string
- params []interface{}
- )
- for _, term := range terms {
- parts := strings.SplitN(term, ":", 2)
- variable := fmt.Sprintf("$%d", param)
- param += 1
- if len(parts) == 1 {
- clause, err := resource.Default(tbl, variable)
- if err != nil {
- return nil, err
- }
- clauses = append(clauses, fmt.Sprintf("(%s)", clause))
- params = append(params, term)
- }
- }
-
- return &WhereClause{strings.Join(clauses, " AND "), params}, nil
-}
M api/graph/model/models_gen.go => api/graph/model/models_gen.go +1 -1
@@ 32,7 32,7 @@ type Artifact struct {
}
type FilterBy struct {
- Terms string `json:"terms"`
+ Search string `json:"search"`
}
type RepoInput struct {
M api/graph/model/repository.go => api/graph/model/repository.go +12 -14
@@ 2,10 2,10 @@ package model
import (
"context"
- "strings"
"time"
"github.com/go-git/go-git/v5"
+ sq "github.com/Masterminds/squirrel"
"git.sr.ht/~sircmpwn/git.sr.ht/api/database"
)
@@ 51,19 51,6 @@ func (r *Repository) Head() *Reference {
return &Reference{Ref: ref, Repo: r.repo}
}
-func (r *Repository) Columns(ctx context.Context, tbl string) string {
- columns := ColumnsFor(ctx, map[string]string{
- "id": "id",
- "created": "created",
- "updated": "updated",
- "name": "name",
- "description": "description",
- "visibility": "visibility",
- "upstreamUrl": "upstream_uri",
- }, tbl)
- return strings.Join(append(columns, tbl+".path", tbl+".owner_id"), ", ")
-}
-
func (r *Repository) Select(ctx context.Context) []string {
return append(database.ColumnsFor(ctx, r.alias, map[string]string{
"id": "id",
@@ 95,3 82,14 @@ func (r *Repository) Fields(ctx context.Context) []interface{} {
})
return append(fields, &r.Path, &r.OwnerID)
}
+
+func (r *Repository) DefaultSearch(query sq.SelectBuilder,
+ term string) (sq.SelectBuilder, error) {
+ name := database.WithAlias(r.alias, "name")
+ desc := database.WithAlias(r.alias, "description")
+ return query.
+ Where(sq.Or{
+ sq.Expr(name + ` ILIKE '%' || ? || '%'`, term),
+ sq.Expr(desc + ` ILIKE '%' || ? || '%'`, term),
+ }), nil
+}
M api/graph/schema.graphqls => api/graph/schema.graphqls +2 -2
@@ 231,7 231,7 @@ type Tag implements Object {
# Specifies filtering criteria for a listing query
input FilterBy {
# Same search syntax as searching on the web UI
- terms: String!
+ search: String!
}
type Query {
@@ 250,7 250,7 @@ type Query {
# will be to return all repositories that the user either (1) has been given
# explicit access to via ACLs or (2) has implicit access to either by
# ownership or group membership.
- repositories(next: Int, filter: FilterBy): [Repository]!
+ repositories(count: Int = 10, next: Int, filter: FilterBy): [Repository]!
# Returns a specific repository
repository(id: Int!): Repository
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +50 -8
@@ 16,7 16,6 @@ import (
"git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model"
"git.sr.ht/~sircmpwn/git.sr.ht/api/loaders"
"git.sr.ht/~sircmpwn/gqlgen/graphql"
- sq "github.com/Masterminds/squirrel"
"github.com/go-git/go-git/v5/plumbing"
)
@@ 75,8 74,43 @@ func (r *queryResolver) User(ctx context.Context, username string) (*model.User,
return loaders.ForContext(ctx).UsersByName.Load(username)
}
-func (r *queryResolver) Repositories(ctx context.Context, next *int, filter *model.FilterBy) ([]*model.Repository, error) {
- panic(fmt.Errorf("not implemented"))
+func (r *queryResolver) Repositories(ctx context.Context, count *int, next *int, filter *model.FilterBy) ([]*model.Repository, error) {
+ var (
+ err error
+ rows *sql.Rows
+ )
+ repo := (&model.Repository{}).As(`repo`)
+ query := database.
+ Select(ctx, repo).
+ From(`repository repo`).
+ Where(`repo.owner_id = ?`, auth.ForContext(ctx).ID).
+ OrderBy(`repo.id DESC`).
+ Limit(uint64(*count))
+ if next != nil {
+ query = query.Where(`repo.id < ?`, *next)
+ }
+ if filter != nil {
+ searchable, _ := repo.(database.Searchable)
+ query, err = database.ApplyFilter(query, searchable, filter.Search)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if rows, err = query.RunWith(r.DB).QueryContext(ctx); err != nil {
+ panic(err)
+ }
+ defer rows.Close()
+
+ var repos []*model.Repository
+ for rows.Next() {
+ var repo model.Repository
+ if err := rows.Scan(repo.Fields(ctx)...); err != nil {
+ panic(err)
+ }
+ repos = append(repos, &repo)
+ }
+ return repos, nil
}
func (r *queryResolver) Repository(ctx context.Context, id int) (*model.Repository, error) {
@@ 142,15 176,23 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, count
err error
rows *sql.Rows
)
+ repo := (&model.Repository{}).As(`repo`)
query := database.
- Select(ctx, (&model.Repository{}).As(`repo`)).
+ Select(ctx, repo).
From(`repository repo`).
- Where(sq.And{
- sq.Expr(`repo.owner_id = ?`, obj.ID),
- sq.Expr(`CASE WHEN ? != 0 THEN repo.id < ? ELSE true END`, next, next),
- }).
+ Where(`repo.owner_id = ?`, obj.ID).
OrderBy(`id DESC`).
Limit(uint64(*count))
+ if next != nil {
+ query = query.Where(`repo.id < ?`, *next)
+ }
+ if filter != nil {
+ searchable, _ := repo.(database.Searchable)
+ query, err = database.ApplyFilter(query, searchable, filter.Search)
+ if err != nil {
+ return nil, err
+ }
+ }
if rows, err = query.RunWith(r.DB).QueryContext(ctx); err != nil {
panic(err)
}