~netlandish/links-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH links] gobwebs-graphql bump

Details
Message ID
<20260818223326.20527-1-peter@netlandish.com>
Sender timestamp
1787070800
DKIM signature
missing
Download raw message
Patch: +128 -414
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
Details
Message ID
<DKSFFJUUFOJJ.3J7VWHF6B0XPG@netlandish.com>
In-Reply-To
<20260818223326.20527-1-peter@netlandish.com> (view parent)
Sender timestamp
1787070932
DKIM signature
missing
Download raw message
Applied.

To git@git.code.netlandish.com:~netlandish/links
   a3ecca4..1df1b08  master -> master
Reply to thread Export thread (mbox)