API: Rig up paste { files }

This commit is contained in:
Drew DeVault 2021-09-21 11:08:40 +02:00
parent fb6d90961a
commit dd5dbf3240
8 changed files with 207 additions and 26 deletions

View File

@ -57,3 +57,6 @@ models:
Cursor:
model:
- git.sr.ht/~sircmpwn/core-go/model.Cursor
URL:
model:
- git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/model.URL

View File

@ -38,6 +38,7 @@ type Config struct {
}
type ResolverRoot interface {
File() FileResolver
Mutation() MutationResolver
Paste() PasteResolver
Query() QueryResolver
@ -101,6 +102,10 @@ type ComplexityRoot struct {
}
}
type FileResolver interface {
Sha(ctx context.Context, obj *model.File) (string, error)
Contents(ctx context.Context, obj *model.File) (*model.URL, error)
}
type MutationResolver interface {
Create(ctx context.Context, files []*graphql.Upload, visibility model.Visibility) (*model.Paste, error)
Update(ctx context.Context, id string, visibility model.Visibility) (*model.Paste, error)
@ -429,6 +434,12 @@ var sources = []*ast.Source{
scalar Cursor
scalar Time
scalar Upload
# URL from which some secondary data may be retrieved. You must provide the
# same Authentication header to this address as you did to the GraphQL resolver
# which provided it. The URL is not guaranteed to be consistent for an extended
# length of time; applications should submit a new GraphQL query each time they
# wish to access the data at the provided URL.
scalar URL
# This is used to decorate fields which are only accessible with a personal
# access token, and are not available to clients using OAuth 2.0 access tokens.
@ -509,7 +520,7 @@ type Paste {
type File {
filename: String!
sha: String!
contents: String!
contents: URL!
}
# A cursor for enumerating pastes
@ -822,14 +833,14 @@ func (ec *executionContext) _File_sha(ctx context.Context, field graphql.Collect
Object: "File",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Sha, nil
return ec.resolvers.File().Sha(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -857,14 +868,14 @@ func (ec *executionContext) _File_contents(ctx context.Context, field graphql.Co
Object: "File",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Contents, nil
return ec.resolvers.File().Contents(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -876,9 +887,9 @@ func (ec *executionContext) _File_contents(ctx context.Context, field graphql.Co
}
return graphql.Null
}
res := resTmp.(string)
res := resTmp.(*model.URL)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
return ec.marshalNURL2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋpasteᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐURL(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_create(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
@ -3197,18 +3208,36 @@ func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj
case "filename":
out.Values[i] = ec._File_filename(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "sha":
out.Values[i] = ec._File_sha(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._File_sha(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "contents":
out.Values[i] = ec._File_contents(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._File_contents(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -3972,6 +4001,32 @@ func (ec *executionContext) marshalNTime2timeᚐTime(ctx context.Context, sel as
return res
}
func (ec *executionContext) unmarshalNURL2gitᚗsrᚗhtᚋאsircmpwnᚋpasteᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐURL(ctx context.Context, v interface{}) (model.URL, error) {
var res model.URL
err := res.UnmarshalGQL(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNURL2gitᚗsrᚗhtᚋאsircmpwnᚋpasteᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐURL(ctx context.Context, sel ast.SelectionSet, v model.URL) graphql.Marshaler {
return v
}
func (ec *executionContext) unmarshalNURL2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋpasteᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐURL(ctx context.Context, v interface{}) (*model.URL, error) {
var res = new(model.URL)
err := res.UnmarshalGQL(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNURL2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋpasteᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐURL(ctx context.Context, sel ast.SelectionSet, v *model.URL) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return v
}
func (ec *executionContext) unmarshalNUpload2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v interface{}) ([]*graphql.Upload, error) {
var vSlice []interface{}
if v != nil {

44
api/graph/model/file.go Normal file
View File

@ -0,0 +1,44 @@
package model
import (
"git.sr.ht/~sircmpwn/core-go/database"
)
type File struct {
Filename string `json:"filename"`
PasteID int
BlobID int
alias string
fields *database.ModelFields
}
func (file *File) As(alias string) *File {
file.alias = alias
return file
}
func (file *File) Alias() string {
return file.alias
}
func (file *File) Table() string {
return "paste_file"
}
func (file *File) Fields() *database.ModelFields {
if file.fields != nil {
return file.fields
}
file.fields = &database.ModelFields{
Fields: []*database.FieldMap{
{ "filename", "filename", &file.Filename },
// Always fetch:
{ "paste_id", "", &file.PasteID },
{ "blob_id", "", &file.BlobID },
},
}
return file.fields
}

View File

@ -15,12 +15,6 @@ type Entity interface {
IsEntity()
}
type File struct {
Filename string `json:"filename"`
Sha string `json:"sha"`
Contents string `json:"contents"`
}
type PasteCursor struct {
Results []*Paste `json:"results"`
Cursor *model.Cursor `json:"cursor"`

View File

@ -18,6 +18,7 @@ type Paste struct {
ID string `json:"id"`
Created time.Time `json:"created"`
PKID int
UserID int
RawVisibility string
@ -56,6 +57,7 @@ func (paste *Paste) Fields() *database.ModelFields {
{ "visibility", "visibility", &paste.RawVisibility },
// Always fetch:
{ "id", "", &paste.PKID },
{ "sha", "", &paste.ID },
{ "user_id", "", &paste.UserID },
},

34
api/graph/model/url.go Normal file
View File

@ -0,0 +1,34 @@
package model
import (
"encoding/json"
"fmt"
"io"
"net/url"
)
// XXX: gqlgen bug prevents us from using type URL *url.URL
type URL struct {
Url *url.URL
}
func (u *URL) UnmarshalGQL(v interface{}) error {
raw, ok := v.(string)
if !ok {
return fmt.Errorf("Mail format is a base64-encoded string")
}
parsed, err := url.Parse(raw)
if err != nil {
return err
}
u.Url = parsed
return nil
}
func (u URL) MarshalGQL(w io.Writer) {
data, err := json.Marshal(u.Url.String())
if err != nil {
panic(err)
}
w.Write(data)
}

View File

@ -3,6 +3,12 @@
scalar Cursor
scalar Time
scalar Upload
# URL from which some secondary data may be retrieved. You must provide the
# same Authentication header to this address as you did to the GraphQL resolver
# which provided it. The URL is not guaranteed to be consistent for an extended
# length of time; applications should submit a new GraphQL query each time they
# wish to access the data at the provided URL.
scalar URL
# This is used to decorate fields which are only accessible with a personal
# access token, and are not available to clients using OAuth 2.0 access tokens.
@ -83,7 +89,7 @@ type Paste {
type File {
filename: String!
sha: String!
contents: String!
contents: URL!
}
# A cursor for enumerating pastes

View File

@ -18,6 +18,14 @@ import (
sq "github.com/Masterminds/squirrel"
)
func (r *fileResolver) Sha(ctx context.Context, obj *model.File) (string, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *fileResolver) Contents(ctx context.Context, obj *model.File) (*model.URL, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) Create(ctx context.Context, files []*graphql.Upload, visibility model.Visibility) (*model.Paste, error) {
panic(fmt.Errorf("not implemented"))
}
@ -31,7 +39,38 @@ func (r *mutationResolver) Delete(ctx context.Context, id string) (*model.Paste,
}
func (r *pasteResolver) Files(ctx context.Context, obj *model.Paste) ([]*model.File, error) {
panic(fmt.Errorf("not implemented"))
var files []*model.File
if err := database.WithTx(ctx, &sql.TxOptions{
Isolation: 0,
ReadOnly: true,
}, func(tx *sql.Tx) error {
file := (&model.File{}).As(`file`)
query := database.
Select(ctx, file).
From(`paste_file file`).
Where(`file.paste_id = ?`, obj.PKID)
rows, err := query.RunWith(tx).QueryContext(ctx)
if err != nil {
return nil
}
defer rows.Close()
for rows.Next() {
var file model.File
if err := rows.Scan(database.Scan(ctx, &file)...); err != nil {
return err
}
files = append(files, &file)
}
return nil
}); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return files, nil
}
func (r *pasteResolver) User(ctx context.Context, obj *model.Paste) (model.Entity, error) {
@ -118,6 +157,9 @@ func (r *userResolver) Pastes(ctx context.Context, obj *model.User, cursor *core
return &model.PasteCursor{pastes, cursor}, nil
}
// File returns api.FileResolver implementation.
func (r *Resolver) File() api.FileResolver { return &fileResolver{r} }
// Mutation returns api.MutationResolver implementation.
func (r *Resolver) Mutation() api.MutationResolver { return &mutationResolver{r} }
@ -130,6 +172,7 @@ func (r *Resolver) Query() api.QueryResolver { return &queryResolver{r} }
// User returns api.UserResolver implementation.
func (r *Resolver) User() api.UserResolver { return &userResolver{r} }
type fileResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type pasteResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }