apicursor

package module
v0.0.0-...-627a6df Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2022 License: Apache-2.0 Imports: 9 Imported by: 0

README

go-mongo-apicursor

A golang implementation of the Relay GraphQL Cursor Connections Specification (https://relay.dev/graphql/connections.htm) for MongoDB. Apache License. Will also work with REST apis to solve the same problem of sharing a cursor to MongoDB query results over a web service request/response.

Prerequisites

If you want to run the linter you will need golangci installed.

HomeBrew: https://brew.sh/

brew install golangci/tap/golangci-lint
Building
make setup
make all
Building for Release
make setup
make release all
Unit Tests
make test
  • Linter
make golint
  • Security Linter
make gosec
Example
type Person struct {
    Id bson.ObjectId `bson:"_id"`
    Name string `bson:"name"`
    CreatedTime time.Time `bson:"createdTime"`
}

type PersonFactory struct {
}

func (df *PersonFactory) New() interface{} {
	return NewPerson()
}

func (df *PersonFactory) NewConnection() apicursor.Connection {
	doc := PersonConnection{}
	return &doc
}

func (df *PersonFactory) NewEdge() apicursor.Edge {
	doc := PersonEdge{}
	return &doc
}

type PersonConnection struct {
	// A list of edges.
	Edges []*PersonEdge
	// A list of nodes.
	Nodes []*Person
	// Information to aid in pagination.
	PageInfo *apicursor.PageInfo
	// Identifies the total count of items in the connection.
	TotalCount uint64
}

func (dc *PersonConnection) GetEdges() []*PersonEdge {
	return dc.Edges
}

func (dc *PersonConnection) SetEdges(edges []apicursor.Edge) {
	dc.Edges = make([]*PersonEdge, len(edges))
	for i, d := range edges {
		de := d.(*PersonEdge)
		dc.Edges[i] = de
	}
}

func (dc *PersonConnection) GetNodes() []*Person {
	return dc.Nodes
}

func (dc *PersonConnection) SetNodes(nodes []interface{}) {
	dc.Nodes = make([]*Person, len(nodes))
	for i, d := range nodes {
		person := d.(*Person)
		dc.Nodes[i] = person
	}
}

func (dc *PersonConnection) GetPageInfo() *apicursor.PageInfo {
	return dc.PageInfo
}

func (dc *PersonConnection) SetPageInfo(pginfo *apicursor.PageInfo) {
	dc.PageInfo = pginfo
}

func (dc *PersonConnection) GetTotalCount() uint64 {
	return dc.TotalCount
}

func (dc *PersonConnection) SetTotalCount(count uint64) {
	dc.TotalCount = count
}

type PersonEdge struct {
	// A cursor for use in pagination.
	Cursor string
	// The item at the end of the edge.
	Node *Person
}

func (de *PersonEdge) GetCursor() string {
	return de.Cursor
}

func (de *PersonEdge) SetCursor(c string) {
	de.Cursor = c
}

func (de *PersonEdge) GetNode() interface{} {
	return de.Node
}

func (de *PersonEdge) SetNode(node interface{}) {
	de.Node = node.(*Person)
}

// PersonOrder Ordering options for Person connections
type PersonOrder struct {
	// The ordering direction.
	Direction apicursor.OrderDirection `json:"direction"`
}

func (r *queryResolver) Persons(ctx context.Context, after *string, before *string, first *int, last *int, orderBy *PersonOrder) (*PersonConnection, error) {
    cursor := apicursor.NewCursor()
	err = cursor.LoadFromAPIRequest(after, before, first, last, &PersonFactory{})
	if err != nil {
		return err
	}
	return person.Instance().Find(
		ctx,
		cursor,
		orderBy,
	)
}

type PersonCursorMarshaler struct{}

func (dcm PersonCursorMarshaler) UnmarshalMongo(c apicursor.APICursor, findFilter bson.M, naturalSortDirection int) (err error) {
	return c.AddCursorFilters(findFilter, naturalSortDirection,
		apicursor.CursorFilterField{
			FieldName: "createdTime",
			FieldType: apicursor.CursorFieldTypeTime,
		},
		apicursor.CursorFilterField{
			FieldName: "_id",
			FieldType: apicursor.CursorFieldTypeMongoOid,
		},
	)
}

func (dcm PersonCursorMarshaler) Marshal(obj interface{}) (cursorFields map[string]string, err error) {
	person := obj.(*Person)
	cursorFields = make(map[string]string)
	cursorFields["createdTime"], err = apicursor.MarshalTimeField(person.CreatedTime)
	cursorFields["_id"] = apicursor.MarshalMongoOidField(person.Id)
	return
}

func (s *personService) Find(ctx context.Context, queryCursor apicursor.APICursor, orderBy *PersonOrder) (personConnection *PersonConnection, err error) {
	if queryCursor == nil {
		queryCursor = apicursor.NewCursor()
	}
	queryCursor.SetMarshaler(&PersonCursorMarshaler{})
	
	filter := bson.M{}
	
	findOptions := &options.FindOptions{}
	findOptions.SetLimit(queryCursor.FindLimit())
	sortDirection := apicursor.DESC.Int()
	if orderBy != nil {
		sortDirection = orderBy.Direction.Int()
	}
	findOptions.SetSort(bson.D{
		{"createdTime", queryCursor.CursorFilterSortDirection(sortDirection)},
		{"_id", queryCursor.CursorFilterSortDirection(sortDirection)},
	})
	
	var countDocsResult int64
	countDocsResult, err = collection.CountDocuments(ctx, filter)
	if err != nil {
		return
	}
	
	err = queryCursor.UnmarshalMongo(filter, sortDirection)
	if err != nil {
		return
	}
	
	var mongoCursor *mongo.Cursor
	mongoCursor, err = collection.Find(ctx, filter, findOptions)
	if err != nil {
		return
	}
	defer func() {
		if mongoCursor != nil {
			_ = mongoCursor.Close(ctx)
		}
	}()

	var connectionResult apicursor.Connection
	connectionResult, err = queryCursor.ConnectionFromMongoCursor(ctx, mongoCursor, countDocsResult)
	if err != nil {
		return
	}
	personConnection = connectionResult.(*PersonConnection)
	return
}

Contributing

Happy to accept PRs.

Author

davidwartell

License

Released under the Apache License.

Documentation

Index

Constants

View Source
const (
	GreaterThanFilterOperator = "$gt"
	LessThanFilterOperator    = "$lt"
)
View Source
const DefaultLimit = int32(10)
View Source
const MaxLimitAllowed = int32(1000)

Variables

This section is empty.

Functions

func MarshalMongoOidField

func MarshalMongoOidField(o primitive.ObjectID) (v string, err error)

func MarshalTimeField

func MarshalTimeField(t time.Time) (v string, err error)

Types

type APICursor

type APICursor interface {
	// LoadFromAPIRequest loads a cursor from query input and sets the modelFactory.
	LoadFromAPIRequest(after *string, before *string, first *int, last *int, modelFactory ModelFactory) (err error)

	// AddCursorFilters adds an ordered list of cursor fields to the filter with logic and is intended to be called by a UnmarshalMongo() implementation.
	AddCursorFilters(findFilter bson.M, naturalSortDirection int, filterFields ...CursorFilterField) (err error)

	// SetTimeCursorFilter attempts to apply fieldName to the filter parsed as a time.Time
	// Deprecated: use AddCursorFilters
	SetTimeCursorFilter(findFilter bson.M, fieldName string, naturalSortDirection int) (err error)

	// SetUUIDCursorFilter attempts to apply fieldName to the filter parsed as a mongouuid.UUID
	// Deprecated: use AddCursorFilters
	SetUUIDCursorFilter(findFilter bson.M, fieldName string, naturalSortDirection int) (err error)

	// SetStringCursorFilter attempts to apply fieldName to the filter parsed as a string
	// Deprecated: use AddCursorFilters
	SetStringCursorFilter(findFilter bson.M, fieldName string, naturalSortDirection int) (err error)

	// FindLimit calculates the limit to a database query based on requested count
	FindLimit() int64

	// CursorFilterSortDirection returns the sort direction (1 if ascending) or (-1 if descending)
	CursorFilterSortDirection(naturalSortDirection int) int

	// UnmarshalMongo unmarshals the cursor and applies it to a mongo map for use in a find filter.
	UnmarshalMongo(findFilter bson.M, naturalSortDirection int) (err error)

	// SetMarshaler sets the CursorMarshaler to use.  This must be set before calling UnmarshalMongo or MarshalMongo, or they will return err.
	SetMarshaler(cursorMarshaler CursorMarshaler)

	// ConnectionFromMongoCursor takes a mongo cursor and returns a connection.  SetMarshaler must be called first.
	ConnectionFromMongoCursor(ctx context.Context, mongoCursor *mongo.Cursor, totalDocsMatching int64) (connection Connection, err error)

	// SetModelFactory sets the modelFactory.
	SetModelFactory(modelFactory ModelFactory)

	// AfterCursor marshals the after cursor to a string.
	AfterCursor() (cursr string, err error)

	// BeforeCursor marshals the after cursor to a string.
	BeforeCursor() (cursr string, err error)
}

func NewCursor

func NewCursor() (c APICursor)

type Connection

type Connection interface {
	// SetEdges sets the edges on the current page.
	SetEdges(edges []Edge)

	// SetNodes sets the nodes for the current page.
	SetNodes(nodes []interface{})

	// GetPageInfo returns the PageInfo
	GetPageInfo() *PageInfo

	// SetPageInfo sets the PageInfo
	SetPageInfo(pginfo *PageInfo)

	// GetTotalCount returns the total count of nodes in the connection (count of all nodes matching the query in all pages).
	GetTotalCount() uint64

	// SetTotalCount sets the TotalCount.
	SetTotalCount(count uint64)
}

Connection implements paging according to spec: https://relay.dev/graphql/connections.htm

type CursorFieldType

type CursorFieldType int
const (
	CursorFieldTypeTime CursorFieldType = iota
	CursorFieldTypeMongoOid
	CursorFieldTypeString
)

func (CursorFieldType) Int

func (t CursorFieldType) Int() int

type CursorFilterField

type CursorFilterField struct {
	FieldName string
	FieldType CursorFieldType
}

type CursorMarshaler

type CursorMarshaler interface {
	// UnmarshalMongo attempts to apply cursors to a mongo filter map if they exist.  Returns an error if the cursor is invalid.
	// c is the cursor to Unmarshal by calling SetTYPECursorFilter() functions. findFilter is the mongo find filter map to apply the cursor to.
	UnmarshalMongo(c APICursor, findFilter bson.M, naturalSortDirection int) (err error)

	// Marshal accepts a model type and returns an error or a map of key values for the document fields to serialize in the cursor.
	Marshal(obj interface{}) (cursorFields map[string]string, err error)
}

CursorMarshaler is used to marshal and unmarshal cursors to and from data store query filters.

type DocumentCursorText

type DocumentCursorText func(document interface{}) (err error)

type Edge

type Edge interface {
	// GetCursor returns the after cursor to load cursor starting at this Edge.
	GetCursor() string

	// SetCursor sets the cursor
	SetCursor(c string)

	// GetNode returns the Node at this Edge.
	GetNode() interface{}

	// SetNode sets the Node for this Edge.
	SetNode(node interface{})
}

type ModelFactory

type ModelFactory interface {
	// New returns a new instance of the object
	New() interface{}

	NewConnection() Connection

	NewEdge() Edge
}

ModelFactory is used to construct new objects when loading results

type OrderDirection

type OrderDirection int
const (
	ASC  OrderDirection = 1
	DESC OrderDirection = -1
)

func (OrderDirection) Int

func (od OrderDirection) Int() int

type PageInfo

type PageInfo struct {
	// When paginating forwards, the cursor to continue.
	EndCursor *string `json:"end_cursor"`

	// When paginating forwards, are there more items?
	HasNextPage bool `json:"has_next_page"`

	// When paginating backwards, are there more items?
	HasPreviousPage bool `json:"has_previous_page"`

	// When paginating backwards, the cursor to continue.
	StartCursor *string `json:"start_cursor"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL