Peter Sanchez: 1 gobwebs-graphql bump 12 files changed, 128 insertions(+), 414 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/235/mbox | git am -3Learn more about email & git
Part of ongoing gobwebs ecosystem updates. This moved valid and cursor modules to the gobwebs-graphql module. Changelog-updated: moved graphql `valid` and `cursor` modules/objects to the latest version of gobwebs-graphql --- api/gqlgen.yml | 2 +- api/graph/generated.go | 65 +++++----- api/graph/model/cursor.go | 59 --------- api/graph/model/models_gen.go | 115 ++++++++--------- api/graph/pagination.go | 17 +-- api/graph/pagination_test.go | 27 ++-- api/graph/schema.resolvers.go | 6 +- go.mod | 2 +- go.sum | 4 +- helpers.go | 10 +- helpers_test.go | 4 +- valid/valid.go | 231 ---------------------------------- 12 files changed, 128 insertions(+), 414 deletions(-) delete mode 100644 api/graph/model/cursor.go delete mode 100644 valid/valid.go diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 6f9e4a4..4b2dd8d 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -52,7 +52,7 @@ autobind: models: Cursor: model: - - links/api/graph/model.Cursor + - netlandish.com/x/gobwebs-graphql/cursor.Cursor ID: model: - github.com/99designs/gqlgen/graphql.ID diff --git a/api/graph/generated.go b/api/graph/generated.go index af1105f..9a3507d 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -20,6 +20,7 @@ import ( gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" "netlandish.com/x/gobwebs-auditlog" + "netlandish.com/x/gobwebs-graphql/cursor" ) // region ************************** generated!.gotpl ************************** @@ -16568,9 +16569,9 @@ func (ec *executionContext) _PageInfo_cursor(ctx context.Context, field graphql. } return graphql.Null } - res := resTmp.(model.Cursor) + res := resTmp.(cursor.Cursor) fc.Result = res - return ec.marshalNCursor2linksᚋapiᚋgraphᚋmodelᚐCursor(ctx, field.Selections, res) + return ec.marshalNCursor2netlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PageInfo_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -25518,14 +25519,14 @@ func (ec *executionContext) unmarshalInputAuditLogInput(ctx context.Context, obj it.ListingID = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -25696,14 +25697,14 @@ func (ec *executionContext) unmarshalInputGetAdminDomainInput(ctx context.Contex it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -25772,14 +25773,14 @@ func (ec *executionContext) unmarshalInputGetAdminOrganizationsInput(ctx context it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -25827,14 +25828,14 @@ func (ec *executionContext) unmarshalInputGetFeedInput(ctx context.Context, obj it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -25910,14 +25911,14 @@ func (ec *executionContext) unmarshalInputGetLinkInput(ctx context.Context, obj it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26014,14 +26015,14 @@ func (ec *executionContext) unmarshalInputGetLinkShortInput(ctx context.Context, it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26090,14 +26091,14 @@ func (ec *executionContext) unmarshalInputGetListingDetailInput(ctx context.Cont it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26138,14 +26139,14 @@ func (ec *executionContext) unmarshalInputGetListingInput(ctx context.Context, o it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26200,14 +26201,14 @@ func (ec *executionContext) unmarshalInputGetOrganizationsInput(ctx context.Cont it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26255,14 +26256,14 @@ func (ec *executionContext) unmarshalInputGetPaymentInput(ctx context.Context, o it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26331,14 +26332,14 @@ func (ec *executionContext) unmarshalInputGetTagsInput(ctx context.Context, obj it.Service = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -26379,14 +26380,14 @@ func (ec *executionContext) unmarshalInputGetUserInput(ctx context.Context, obj it.Limit = data case "after": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } it.After = data case "before": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - data, err := ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + data, err := ec.unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx, v) if err != nil { return it, err } @@ -32193,13 +32194,13 @@ func (ec *executionContext) marshalNCloudOrderType2linksᚋapiᚋgraphᚋmodel return v } -func (ec *executionContext) unmarshalNCursor2linksᚋapiᚋgraphᚋmodelᚐCursor(ctx context.Context, v any) (model.Cursor, error) { - var res model.Cursor +func (ec *executionContext) unmarshalNCursor2netlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx context.Context, v any) (cursor.Cursor, error) { + var res cursor.Cursor err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNCursor2linksᚋapiᚋgraphᚋmodelᚐCursor(ctx context.Context, sel ast.SelectionSet, v model.Cursor) graphql.Marshaler { +func (ec *executionContext) marshalNCursor2netlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx context.Context, sel ast.SelectionSet, v cursor.Cursor) graphql.Marshaler { return v } @@ -33601,16 +33602,16 @@ func (ec *executionContext) unmarshalOCompleteRegisterInput2ᚖlinksᚋapiᚋgra return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx context.Context, v any) (*model.Cursor, error) { +func (ec *executionContext) unmarshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx context.Context, v any) (*cursor.Cursor, error) { if v == nil { return nil, nil } - var res = new(model.Cursor) + var res = new(cursor.Cursor) err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx context.Context, sel ast.SelectionSet, v *model.Cursor) graphql.Marshaler { +func (ec *executionContext) marshalOCursor2ᚖnetlandishᚗcomᚋxᚋgobwebsᚑgraphqlᚋcursorᚐCursor(ctx context.Context, sel ast.SelectionSet, v *cursor.Cursor) graphql.Marshaler { if v == nil { return graphql.Null } diff --git a/api/graph/model/cursor.go b/api/graph/model/cursor.go deleted file mode 100644 index f4e05eb..0000000 --- a/api/graph/model/cursor.go @@ -1,59 +0,0 @@ -package model - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "io" - "links/valid" - - "github.com/vektah/gqlparser/v2/gqlerror" -) - -const ( - PaginationDefault int = 25 - PaginationMax int = 100 -) - -// Cursor is used to represent a graph cursor in the resolver -// as well in the handler parsin data -type Cursor struct { - After int - Before int - Limit int -} - -// errInvalidCursor carries a validation code so consumers running the error -// through ParseInputErrors render a form error rather than falling through to -// the raw error path, which the web tier reports as a 500. -func errInvalidCursor() error { - return &gqlerror.Error{ - Message: "Invalid cursor", - Extensions: map[string]any{"code": valid.ErrValidationGlobalCode}, - } -} - -func (c *Cursor) UnmarshalGQL(v any) error { - enc, ok := v.(string) - if !ok { - return fmt.Errorf("cursor must be strings") - } - plain, err := base64.StdEncoding.DecodeString(enc) - if plain == nil || err != nil { - return errInvalidCursor() - } - if err = json.Unmarshal(plain, c); err != nil { - return errInvalidCursor() - } - return nil -} - -func (c Cursor) MarshalGQL(w io.Writer) { - data, err := json.Marshal(c) - if err != nil { - panic(err) - } - w.Write([]byte("\"")) - w.Write([]byte(base64.StdEncoding.EncodeToString(data))) - w.Write([]byte("\"")) -} diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index 79ddf39..fb3b6f5 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -12,6 +12,7 @@ import ( "github.com/99designs/gqlgen/graphql" "netlandish.com/x/gobwebs-auditlog" + "netlandish.com/x/gobwebs-graphql/cursor" ) type AddListingInput struct { @@ -114,12 +115,12 @@ type AuditLogCursor struct { } type AuditLogInput struct { - UserID *int `json:"userId,omitempty"` - OrgSlug *string `json:"orgSlug,omitempty"` - ListingID *int `json:"listingId,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Limit *int `json:"limit,omitempty"` + UserID *int `json:"userId,omitempty"` + OrgSlug *string `json:"orgSlug,omitempty"` + ListingID *int `json:"listingId,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Limit *int `json:"limit,omitempty"` } type BookmarkCursor struct { @@ -169,8 +170,8 @@ type EmailPostUser struct { type GetAdminDomainInput struct { Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` Search *string `json:"search,omitempty"` FilterLevel *DomainLevel `json:"filterLevel,omitempty"` FilterService *DomainService `json:"filterService,omitempty"` @@ -179,17 +180,17 @@ type GetAdminDomainInput struct { } type GetAdminOrganizationsInput struct { - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Search *string `json:"search,omitempty"` - OwnerID *int `json:"ownerId,omitempty"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Search *string `json:"search,omitempty"` + OwnerID *int `json:"ownerId,omitempty"` } type GetFeedInput struct { Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` Tag *string `json:"tag,omitempty"` ExcludeTag *string `json:"excludeTag,omitempty"` Search *string `json:"search,omitempty"` @@ -200,8 +201,8 @@ type GetFeedInput struct { type GetLinkInput struct { OrgSlug *string `json:"orgSlug,omitempty"` Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` Tag *string `json:"tag,omitempty"` ExcludeTag *string `json:"excludeTag,omitempty"` Search *string `json:"search,omitempty"` @@ -213,64 +214,64 @@ type GetLinkInput struct { } type GetLinkShortInput struct { - OrgSlug string `json:"orgSlug"` - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Tag *string `json:"tag,omitempty"` - ExcludeTag *string `json:"excludeTag,omitempty"` - DomainID *int `json:"domainId,omitempty"` + OrgSlug string `json:"orgSlug"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Tag *string `json:"tag,omitempty"` + ExcludeTag *string `json:"excludeTag,omitempty"` + DomainID *int `json:"domainId,omitempty"` } type GetListingDetailInput struct { - DomainID int `json:"domainId"` - Slug *string `json:"slug,omitempty"` - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` + DomainID int `json:"domainId"` + Slug *string `json:"slug,omitempty"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` } type GetListingInput struct { - OrgSlug string `json:"orgSlug"` - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Tag *string `json:"tag,omitempty"` - ExcludeTag *string `json:"excludeTag,omitempty"` - DomainID *int `json:"domainId,omitempty"` + OrgSlug string `json:"orgSlug"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Tag *string `json:"tag,omitempty"` + ExcludeTag *string `json:"excludeTag,omitempty"` + DomainID *int `json:"domainId,omitempty"` } type GetOrganizationsInput struct { - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Search *string `json:"search,omitempty"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Search *string `json:"search,omitempty"` } type GetPaymentInput struct { - OrgSlug *string `json:"orgSlug,omitempty"` - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Interval *int `json:"interval,omitempty"` - DateStart *string `json:"dateStart,omitempty"` - DateEnd *string `json:"dateEnd,omitempty"` - Filter *bool `json:"filter,omitempty"` + OrgSlug *string `json:"orgSlug,omitempty"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Interval *int `json:"interval,omitempty"` + DateStart *string `json:"dateStart,omitempty"` + DateEnd *string `json:"dateEnd,omitempty"` + Filter *bool `json:"filter,omitempty"` } type GetTagsInput struct { OrgSlug string `json:"orgSlug"` Service *DomainService `json:"service,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` Limit *int `json:"limit,omitempty"` } type GetUserInput struct { - Limit *int `json:"limit,omitempty"` - After *Cursor `json:"after,omitempty"` - Before *Cursor `json:"before,omitempty"` - Search *string `json:"search,omitempty"` + Limit *int `json:"limit,omitempty"` + After *cursor.Cursor `json:"after,omitempty"` + Before *cursor.Cursor `json:"before,omitempty"` + Search *string `json:"search,omitempty"` } type LinkInput struct { @@ -371,9 +372,9 @@ type OrganizationStats struct { // This is used in various cursor objects for pagination type PageInfo struct { - Cursor Cursor `json:"cursor"` - HasNextPage bool `json:"hasNextPage"` - HasPrevPage bool `json:"hasPrevPage"` + Cursor cursor.Cursor `json:"cursor"` + HasNextPage bool `json:"hasNextPage"` + HasPrevPage bool `json:"hasPrevPage"` } type Payment struct { diff --git a/api/graph/pagination.go b/api/graph/pagination.go index 09068e1..9727afa 100644 --- a/api/graph/pagination.go +++ b/api/graph/pagination.go @@ -5,6 +5,7 @@ import ( "links/api/graph/model" sq "github.com/Masterminds/squirrel" + "netlandish.com/x/gobwebs-graphql/cursor" "netlandish.com/x/gobwebs/database" ) @@ -14,7 +15,7 @@ type contextKey struct { name string } -// PaginationContext is used to override `model.PaginationMax` value +// PaginationContext is used to override `cursor.PaginationMax` value func PaginationContext(ctx context.Context, limit int) context.Context { return context.WithValue(ctx, paginationCtxKey, limit) } @@ -23,12 +24,12 @@ func PaginationContext(ctx context.Context, limit int) context.Context { func ForPaginationContext(ctx context.Context) int { limit, ok := ctx.Value(paginationCtxKey).(int) if !ok { - return model.PaginationMax + return cursor.PaginationMax } return limit } -func PaginateResults[T any](items []T, limit int, before, after *model.Cursor, +func PaginateResults[T any](items []T, limit int, before, after *cursor.Cursor, getID func(T) int) ([]T, *model.PageInfo) { overFetched := len(items) > limit if overFetched { @@ -57,7 +58,7 @@ func PaginateResults[T any](items []T, limit int, before, after *model.Cursor, return items, &pageInfo } -func cursorField(before, after *model.Cursor, idField, orderDir string) sq.Sqlizer { +func cursorField(before, after *cursor.Cursor, idField, orderDir string) sq.Sqlizer { if after != nil { if orderDir == "DESC" { return sq.Lt{idField: after.After} @@ -80,11 +81,11 @@ func QueryModel[T any]( opts *database.FilterOptions, idField, orderDir string, limit *int, - before, after *model.Cursor, + before, after *cursor.Cursor, getModels func(context.Context, *database.FilterOptions) ([]T, error), getID func(T) int, ) ([]T, *model.PageInfo, error) { - numElements := model.PaginationDefault + numElements := cursor.PaginationDefault if after != nil { opts.Filter = sq.And{ opts.Filter, @@ -114,10 +115,10 @@ func QueryModel[T any]( // would drop the SQL LIMIT clause entirely in GetBuilder and then panic the // slice reslice in PaginateResults. if numElements <= 0 { - numElements = model.PaginationDefault + numElements = cursor.PaginationDefault } - // maxLimit will default to model.PaginationMax + // maxLimit will default to cursor.PaginationMax maxLimit := ForPaginationContext(ctx) if numElements > maxLimit { numElements = maxLimit diff --git a/api/graph/pagination_test.go b/api/graph/pagination_test.go index 6307936..86bae1c 100644 --- a/api/graph/pagination_test.go +++ b/api/graph/pagination_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "netlandish.com/x/gobwebs-graphql/cursor" "netlandish.com/x/gobwebs/database" ) @@ -16,7 +17,7 @@ func TestPaginationForwardAndBackward(t *testing.T) { allItems = append(allItems, i) } - getPage := func(after, before *model.Cursor) ([]int, *model.PageInfo) { + getPage := func(after, before *cursor.Cursor) ([]int, *model.PageInfo) { var filtered []int for _, id := range allItems { // ORDER BY id DESC @@ -38,7 +39,7 @@ func TestPaginationForwardAndBackward(t *testing.T) { } t.Run("Forward Pagination", func(t *testing.T) { - var after *model.Cursor + var after *cursor.Cursor seen := make(map[int]bool) for { page, info := getPage(after, nil) @@ -52,13 +53,13 @@ func TestPaginationForwardAndBackward(t *testing.T) { if !info.HasNextPage { break } - after = &model.Cursor{After: info.Cursor.After, Limit: 5} + after = &cursor.Cursor{After: info.Cursor.After, Limit: 5} } assert.Equal(t, 26, len(seen)) }) t.Run("Backward Pagination", func(t *testing.T) { - before := &model.Cursor{Before: 0} + before := &cursor.Cursor{Before: 0} seen := make(map[int]bool) for { page, info := getPage(nil, before) @@ -72,7 +73,7 @@ func TestPaginationForwardAndBackward(t *testing.T) { if !info.HasPrevPage { break } - before = &model.Cursor{Before: info.Cursor.Before, Limit: 5} + before = &cursor.Cursor{Before: info.Cursor.Before, Limit: 5} } assert.Equal(t, 26, len(seen)) }) @@ -84,7 +85,7 @@ func TestQueryModelClampsCursorLimit(t *testing.T) { items = append(items, i) } - run := func(t *testing.T, limit *int, before, after *model.Cursor) int { + run := func(t *testing.T, limit *int, before, after *cursor.Cursor) int { t.Helper() var gotLimit int getModels := func(_ context.Context, opts *database.FilterOptions) ([]int, error) { @@ -103,22 +104,22 @@ func TestQueryModelClampsCursorLimit(t *testing.T) { // A negative cursor limit used to leave opts.Limit at zero, which drops the SQL // LIMIT clause entirely and then panics on the reslice in PaginateResults. t.Run("negative after limit", func(t *testing.T) { - assert.Equal(t, model.PaginationDefault+1, - run(t, nil, nil, &model.Cursor{After: 150, Limit: -1})) + assert.Equal(t, cursor.PaginationDefault+1, + run(t, nil, nil, &cursor.Cursor{After: 150, Limit: -1})) }) t.Run("zero before limit", func(t *testing.T) { - assert.Equal(t, model.PaginationDefault+1, - run(t, nil, &model.Cursor{Before: 150, Limit: 0}, nil)) + assert.Equal(t, cursor.PaginationDefault+1, + run(t, nil, &cursor.Cursor{Before: 150, Limit: 0}, nil)) }) t.Run("negative explicit limit falls back to cursor", func(t *testing.T) { negative := -5 - assert.Equal(t, 11, run(t, &negative, nil, &model.Cursor{After: 150, Limit: 10})) + assert.Equal(t, 11, run(t, &negative, nil, &cursor.Cursor{After: 150, Limit: 10})) }) t.Run("oversized cursor limit still capped", func(t *testing.T) { - assert.Equal(t, model.PaginationMax+1, - run(t, nil, nil, &model.Cursor{After: 150, Limit: 10000})) + assert.Equal(t, cursor.PaginationMax+1, + run(t, nil, nil, &cursor.Cursor{After: 150, Limit: 10000})) }) } diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index bdb6ccc..1033be8 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -23,7 +23,6 @@ import ( "links/domain" "links/internal/localizer" "links/models" - "links/valid" "net/url" "os" "regexp" @@ -40,8 +39,9 @@ import ( "golang.org/x/image/draw" "golang.org/x/net/idna" "netlandish.com/x/gobwebs" - auditlog "netlandish.com/x/gobwebs-auditlog" - oauth2 "netlandish.com/x/gobwebs-oauth2" + "netlandish.com/x/gobwebs-auditlog" + "netlandish.com/x/gobwebs-graphql/valid" + "netlandish.com/x/gobwebs-oauth2" gaccounts "netlandish.com/x/gobwebs/accounts" "netlandish.com/x/gobwebs/crypto" "netlandish.com/x/gobwebs/database" diff --git a/go.mod b/go.mod index 4c3c217..e6b935e 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( netlandish.com/x/gobwebs v0.1.15 netlandish.com/x/gobwebs-auditlog v0.3.2 netlandish.com/x/gobwebs-formguard v0.2.13 - netlandish.com/x/gobwebs-graphql v0.2.14 + netlandish.com/x/gobwebs-graphql v0.3.0 netlandish.com/x/gobwebs-oauth2 v0.2.13 netlandish.com/x/gobwebs-ses-feedback v0.2.13 petersanchez.com/x/carrier v0.2.4 diff --git a/go.sum b/go.sum index d2cccaf..4469686 100644 --- a/go.sum +++ b/go.sum @@ -2588,8 +2588,8 @@ netlandish.com/x/gobwebs-auditlog v0.3.2 h1:mG3y/gA9wLFbm172JAg3qhQXsviL7AEUw1TA netlandish.com/x/gobwebs-auditlog v0.3.2/go.mod h1:Hp/tmzsy6Cum7w/s3xeA7iXHOkTVCxLYmU7mEXJZuL4= netlandish.com/x/gobwebs-formguard v0.2.13 h1:bqolzDBFLYk9V3qglIXccMEefIoFlCHrte7UjRyN9EY= netlandish.com/x/gobwebs-formguard v0.2.13/go.mod h1:3bH1yWdbFauqMr+CP5hmsOSt4VHIY4qfo4wPQfRnasg= -netlandish.com/x/gobwebs-graphql v0.2.14 h1:rQ98bzgEat9/FSbKTqYwJi6juIQ2Rh1RTc50dG2XAl8= -netlandish.com/x/gobwebs-graphql v0.2.14/go.mod h1:T45dDN3aIbpOX+rjAMyxa/yZuB3HZqgPAyEEypVWIiE= +netlandish.com/x/gobwebs-graphql v0.3.0 h1:E8v+R4TYBezP1zgbXgK7aLFA+jQAi1VIlz6l+DucOis= +netlandish.com/x/gobwebs-graphql v0.3.0/go.mod h1:ihf/UpV46TJSVgGw0avUeMw/YANC08BzOZJ+fvk+zyM= netlandish.com/x/gobwebs-oauth2 v0.2.13 h1:Wco3mWNzXIq5PxdgrvWLXKO6kCH6G0GiHJgkyc9U7KI= netlandish.com/x/gobwebs-oauth2 v0.2.13/go.mod h1:4+LM3EHqQ0gtONbVdExgZBNckI/Pmx23XELZiXv7fpw= netlandish.com/x/gobwebs-ses-feedback v0.2.13 h1:PFKHqv4cOfOG+rx5zQnl/tmYYQRrW+SP1SaKJ29b1e8= diff --git a/helpers.go b/helpers.go index ceaf027..6617434 100644 --- a/helpers.go +++ b/helpers.go @@ -9,11 +9,9 @@ import ( "fmt" "html/template" "io" - "links/api/graph/model" "links/domain" "links/internal/localizer" "links/models" - "links/valid" "mime/multipart" "net" "net/http" @@ -40,6 +38,8 @@ import ( "golang.org/x/time/rate" "netlandish.com/x/gobwebs" auditlog "netlandish.com/x/gobwebs-auditlog" + "netlandish.com/x/gobwebs-graphql/cursor" + "netlandish.com/x/gobwebs-graphql/valid" "netlandish.com/x/gobwebs/config" "netlandish.com/x/gobwebs/core" "netlandish.com/x/gobwebs/crypto" @@ -501,7 +501,7 @@ func ParsePendingBaseURLs(ctx context.Context, userAgent string) error { } func ValidCursor(v string) bool { - var cur model.Cursor + var cur cursor.Cursor return cur.UnmarshalGQL(v) == nil } @@ -530,7 +530,7 @@ func SafeRedirect(next, fallback string) string { } // GetPaginationParams returns the params needed for cursor pagination -func GetPaginationParams(c echo.Context, pagvar, cursor string, exclude ...string) template.URL { +func GetPaginationParams(c echo.Context, pagvar, cur string, exclude ...string) template.URL { q := make(url.Values) oldq := c.QueryParams() for k, val := range oldq { @@ -541,7 +541,7 @@ func GetPaginationParams(c echo.Context, pagvar, cursor string, exclude ...strin q.Add(k, v) } } - q.Set(pagvar, cursor) + q.Set(pagvar, cur) return template.URL(q.Encode()) } diff --git a/helpers_test.go b/helpers_test.go index 2cf5938..710ddcf 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -2,7 +2,6 @@ package links import ( "encoding/base64" - "links/api/graph/model" "net/http" "net/http/httptest" "net/url" @@ -11,11 +10,12 @@ import ( "git.sr.ht/~emersion/gqlclient" "github.com/labstack/echo/v4" "github.com/stretchr/testify/require" + "netlandish.com/x/gobwebs-graphql/cursor" ) func validCursor(t *testing.T) string { t.Helper() - cur := model.Cursor{After: 199071, Before: 207789, Limit: 25} + cur := cursor.Cursor{After: 199071, Before: 207789, Limit: 25} rec := httptest.NewRecorder() cur.MarshalGQL(rec.Body) return rec.Body.String()[1 : rec.Body.Len()-1] diff --git a/valid/valid.go b/valid/valid.go deleted file mode 100644 index 30210d4..0000000 --- a/valid/valid.go @@ -1,231 +0,0 @@ -package valid - -import ( - "context" - "errors" - "fmt" - - "github.com/99designs/gqlgen/graphql" - "github.com/vektah/gqlparser/v2/gqlerror" -) - -// ErrValidationCode ... -const ( - ErrValidationCode int = 100 - ErrNotFoundCode int = 200 - ErrValidationGlobalCode int = 300 - ErrRestrictedCode int = 400 -) - -var ( - // ErrAuthorization ... - ErrAuthorization = errors.New("Authorizatoin required") -) - -// Validation ... -type Validation struct { - ctx context.Context - input map[string]any -} - -// ValidationError ... -type ValidationError struct { - valid *Validation - err *gqlerror.Error -} - -// Error Returns a new GraphQL error attached to the given field. -func Error(ctx context.Context, field string, msg string) error { - return &gqlerror.Error{ - Message: msg, - Path: graphql.GetPath(ctx), - Extensions: map[string]any{ - "field": field, - }, - } -} - -// Errorf Returns a new GraphQL error attached to the given field. -func Errorf(ctx context.Context, field string, msg string, items ...any) error { - return &gqlerror.Error{ - Message: fmt.Sprintf(msg, items...), - Path: graphql.GetPath(ctx), - Extensions: map[string]any{ - "field": field, - }, - } -} - -// New Creates a new validation context. -func New(ctx context.Context) *Validation { - return &Validation{ - ctx: ctx, - } -} - -// WithInput Adds an input map to a validation context. -func (valid *Validation) WithInput(input map[string]any) *Validation { - valid.input = input - return valid -} - -// Ok Returns true if no errors were found. -func (valid *Validation) Ok() bool { - return len(graphql.GetErrors(valid.ctx)) == 0 -} - -// Optional Fetches an item from the validation context, which must have an input -// registered. If the field is not present, the callback is not run. Otherwise, -// the function is called with the value for the user to conduct further -// validation with. -func (valid *Validation) Optional(name string, fn func(i any)) { - if valid.input == nil { - panic("Attempted to validate fields without input") - } - if o, ok := valid.input[name]; ok { - if o == nil { - return - } - fn(o) - } -} - -// OptionalString Fetches a string from the validation context, which must have an input -// registered. If the field is not present, the callback is not run. If -// present, but not a string, an error is recorded. Otherwise, the function is -// called with the string for the user to conduct further validation with. -func (valid *Validation) OptionalString(name string, fn func(s string)) { - if valid.input == nil { - panic("Attempted to validate fields without input") - } - if o, ok := valid.input[name]; ok { - if o == nil { - return - } - var val string - switch s := o.(type) { - case string: - val = s - case *string: - val = *s - default: - valid. - Error("Expected %s to be a string", name). - WithField(name) - return - } - fn(val) - } -} - -// NullableString Fetches a nullable string from the validation context, which must have an -// input registered. If the field is not present, the callback is not run. If -// present, but null, the function is called with null set to true. Otherwise, -// the function is called with the string for the user to conduct further -// validation with. -func (valid *Validation) NullableString(name string, fn func(s *string)) { - if valid.input == nil { - panic("Attempted to validate fields without input") - } - if o, ok := valid.input[name]; ok { - var val *string - if o != nil { - switch s := o.(type) { - case string: - val = &s - case *string: - val = s - default: - valid. - Error("Expected %s to be a string", name). - WithField(name) - return - } - } - fn(val) - } -} - -// OptionalBool Fetches a boolean from the validation context, which must have an input -// registered. If the field is not present, the callback is not run. If -// present, but not a boolean, an error is recorded. Otherwise, the function is -// called with the boolean for the user to conduct further validation with. -func (valid *Validation) OptionalBool(name string, fn func(b bool)) { - if valid.input == nil { - panic("Attempted to validate fields without input") - } - if o, ok := valid.input[name]; ok { - if o == nil { - return - } - var val bool - switch b := o.(type) { - case bool: - val = b - case *bool: - val = *b - default: - valid. - Error("Expected %s to be a bool", name). - WithField(name) - return - } - fn(val) - } -} - -// Creates a validation error unconditionally. -func (valid *Validation) Error(msg string, items ...any) *ValidationError { - err := &gqlerror.Error{ - Path: graphql.GetPath(valid.ctx), - Message: fmt.Sprintf(msg, items...), - } - graphql.AddError(valid.ctx, err) - return &ValidationError{ - valid: valid, - err: err, - } -} - -// Expect Asserts that a condition is true, recording a GraphQL error with the given -// message if not. -func (valid *Validation) Expect(cond bool, msg string, items ...any) *ValidationError { - if cond { - return &ValidationError{valid: valid} - } - return valid.Error(msg, items...) -} - -// WithField Associates a field name with an error. -func (err *ValidationError) WithField(field string) *ValidationError { - if err.err == nil { - return err - } - if err.err.Extensions == nil { - err.err.Extensions = make(map[string]any) - } - err.err.Extensions["field"] = field - return err -} - -// WithCode Associates an error message with an error code. -func (err *ValidationError) WithCode(code int) *ValidationError { - if err.err == nil { - return err - } - if err.err.Extensions == nil { - err.err.Extensions = make(map[string]any) - } - err.err.Extensions["code"] = code - return err -} - -// And Composes another assertion onto the same validation context which initially -// created an error. Short-circuiting is used, such that if the earlier -// condition failed, the new condition is not considered. -func (err *ValidationError) And(cond bool, msg string, items ...any) *ValidationError { - if err.err != nil { - return err - } - return err.valid.Expect(cond, msg, items...) -} -- 2.54.0
Applied. To git@git.code.netlandish.com:~netlandish/links a3ecca4..1df1b08 master -> master