Add framework resolver impl

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-01-22 16:13:36 +01:00
parent b749c1277a
commit 8bb1911156
15 changed files with 614 additions and 68 deletions

3
go.mod
View File

@@ -3,10 +3,12 @@ module github.com/getprobo/probo
go 1.23.4
require (
gearno.de/ref v0.0.0-20221013162104-a522beda40f4
github.com/99designs/gqlgen v0.17.63
github.com/jackc/pgx/v5 v5.7.1
github.com/prometheus/client_golang v1.20.5
github.com/vektah/gqlparser/v2 v2.5.21
go.gearno.de/crypto/uuid v0.1.0
go.gearno.de/kit v0.0.0-20241119134016-ee4f36699e6b
go.opentelemetry.io/otel/trace v1.32.0
)
@@ -34,7 +36,6 @@ require (
github.com/sosodev/duration v1.3.1 // indirect
github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.gearno.de/crypto/uuid v0.1.0 // indirect
go.gearno.de/x/panicf v0.1.1 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect

2
go.sum
View File

@@ -1,3 +1,5 @@
gearno.de/ref v0.0.0-20221013162104-a522beda40f4 h1:tS3oPI89+Y33vZRR/DwbNTewefycz1UzEeDYgMKNdzY=
gearno.de/ref v0.0.0-20221013162104-a522beda40f4/go.mod h1:yMxgb+Im8XTCLBBgpy8p5xE6/OaWkKyfypNd3HPQpLw=
github.com/99designs/gqlgen v0.17.63 h1:HCdaYDPd9HqUXRchEvmE3EFzELRwLlaJ8DBuyC8Cqto=
github.com/99designs/gqlgen v0.17.63/go.mod h1:sVCM2iwIZisJjTI/DEC3fpH+HFgxY1496ZJ+jbT9IjA=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=

View File

@@ -22,3 +22,6 @@ models:
Datetime:
model:
- "github.com/99designs/gqlgen/graphql.Time"
CursorKey:
model:
- "github.com/getprobo/probo/pkg/api/console/v1/types.CursorKeyScalar"

View File

@@ -2,6 +2,10 @@
package console_v1
import "github.com/getprobo/probo/pkg/probo"
type (
Resolver struct{}
Resolver struct {
svc *probo.Service
}
)

View File

@@ -15,6 +15,7 @@ import (
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/introspection"
"github.com/getprobo/probo/pkg/api/console/v1/types"
"github.com/getprobo/probo/pkg/probo/coredata/page"
gqlparser "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
)
@@ -54,7 +55,7 @@ type ComplexityRoot struct {
Description func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
Tasks func(childComplexity int, first *int, after *string, last *int, before *string) int
Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
UpdatedAt func(childComplexity int) int
}
@@ -69,7 +70,7 @@ type ComplexityRoot struct {
}
Framework struct {
Controls func(childComplexity int, first *int, after *string, last *int, before *string) int
Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
ID func(childComplexity int) int
@@ -89,7 +90,7 @@ type ComplexityRoot struct {
Organization struct {
CreatedAt func(childComplexity int) int
Frameworks func(childComplexity int, first *int, after *string, last *int, before *string) int
Frameworks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
ID func(childComplexity int) int
Name func(childComplexity int) int
UpdatedAt func(childComplexity int) int
@@ -125,13 +126,13 @@ type ComplexityRoot struct {
}
type ControlResolver interface {
Tasks(ctx context.Context, obj *types.Control, first *int, after *string, last *int, before *string) (*types.TaskConnection, error)
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error)
}
type FrameworkResolver interface {
Controls(ctx context.Context, obj *types.Framework, first *int, after *string, last *int, before *string) (*types.ControlConnection, error)
Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlConnection, error)
}
type OrganizationResolver interface {
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *string, last *int, before *string) (*types.FrameworkConnection, error)
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error)
}
type QueryResolver interface {
Node(ctx context.Context, id string) (types.Node, error)
@@ -194,7 +195,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Control.Tasks(childComplexity, args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true
return e.complexity.Control.Tasks(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
case "Control.updatedAt":
if e.complexity.Control.UpdatedAt == nil {
@@ -241,7 +242,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Framework.Controls(childComplexity, args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true
return e.complexity.Framework.Controls(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
case "Framework.createdAt":
if e.complexity.Framework.CreatedAt == nil {
@@ -323,7 +324,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Organization.Frameworks(childComplexity, args["first"].(*int), args["after"].(*string), args["last"].(*int), args["before"].(*string)), true
return e.complexity.Organization.Frameworks(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
case "Organization.id":
if e.complexity.Organization.ID == nil {
@@ -688,13 +689,13 @@ func (ec *executionContext) field_Control_tasks_argsFirst(
func (ec *executionContext) field_Control_tasks_argsAfter(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
if tmp, ok := rawArgs["after"]; ok {
return ec.unmarshalOCursorKey2ᚖstring(ctx, tmp)
return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp)
}
var zeroVal *string
var zeroVal *page.CursorKey
return zeroVal, nil
}
@@ -714,13 +715,13 @@ func (ec *executionContext) field_Control_tasks_argsLast(
func (ec *executionContext) field_Control_tasks_argsBefore(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
if tmp, ok := rawArgs["before"]; ok {
return ec.unmarshalOCursorKey2ᚖstring(ctx, tmp)
return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp)
}
var zeroVal *string
var zeroVal *page.CursorKey
return zeroVal, nil
}
@@ -765,13 +766,13 @@ func (ec *executionContext) field_Framework_controls_argsFirst(
func (ec *executionContext) field_Framework_controls_argsAfter(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
if tmp, ok := rawArgs["after"]; ok {
return ec.unmarshalOCursorKey2ᚖstring(ctx, tmp)
return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp)
}
var zeroVal *string
var zeroVal *page.CursorKey
return zeroVal, nil
}
@@ -791,13 +792,13 @@ func (ec *executionContext) field_Framework_controls_argsLast(
func (ec *executionContext) field_Framework_controls_argsBefore(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
if tmp, ok := rawArgs["before"]; ok {
return ec.unmarshalOCursorKey2ᚖstring(ctx, tmp)
return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp)
}
var zeroVal *string
var zeroVal *page.CursorKey
return zeroVal, nil
}
@@ -842,13 +843,13 @@ func (ec *executionContext) field_Organization_frameworks_argsFirst(
func (ec *executionContext) field_Organization_frameworks_argsAfter(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
if tmp, ok := rawArgs["after"]; ok {
return ec.unmarshalOCursorKey2ᚖstring(ctx, tmp)
return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp)
}
var zeroVal *string
var zeroVal *page.CursorKey
return zeroVal, nil
}
@@ -868,13 +869,13 @@ func (ec *executionContext) field_Organization_frameworks_argsLast(
func (ec *executionContext) field_Organization_frameworks_argsBefore(
ctx context.Context,
rawArgs map[string]any,
) (*string, error) {
) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
if tmp, ok := rawArgs["before"]; ok {
return ec.unmarshalOCursorKey2ᚖstring(ctx, tmp)
return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp)
}
var zeroVal *string
var zeroVal *page.CursorKey
return zeroVal, nil
}
@@ -1100,7 +1101,7 @@ func (ec *executionContext) _Control_tasks(ctx context.Context, field graphql.Co
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Control().Tasks(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*string), fc.Args["last"].(*int), fc.Args["before"].(*string))
return ec.resolvers.Control().Tasks(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey))
})
if err != nil {
ec.Error(ctx, err)
@@ -1329,9 +1330,9 @@ func (ec *executionContext) _ControlEdge_cursor(ctx context.Context, field graph
}
return graphql.Null
}
res := resTmp.(string)
res := resTmp.(page.CursorKey)
fc.Result = res
return ec.marshalNCursorKey2string(ctx, field.Selections, res)
return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_ControlEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1521,7 +1522,7 @@ func (ec *executionContext) _Framework_controls(ctx context.Context, field graph
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Framework().Controls(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*string), fc.Args["last"].(*int), fc.Args["before"].(*string))
return ec.resolvers.Framework().Controls(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey))
})
if err != nil {
ec.Error(ctx, err)
@@ -1750,9 +1751,9 @@ func (ec *executionContext) _FrameworkEdge_cursor(ctx context.Context, field gra
}
return graphql.Null
}
res := resTmp.(string)
res := resTmp.(page.CursorKey)
fc.Result = res
return ec.marshalNCursorKey2string(ctx, field.Selections, res)
return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_FrameworkEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1904,7 +1905,7 @@ func (ec *executionContext) _Organization_frameworks(ctx context.Context, field
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Organization().Frameworks(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*string), fc.Args["last"].(*int), fc.Args["before"].(*string))
return ec.resolvers.Organization().Frameworks(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey))
})
if err != nil {
ec.Error(ctx, err)
@@ -2114,9 +2115,9 @@ func (ec *executionContext) _PageInfo_startCursor(ctx context.Context, field gra
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
res := resTmp.(*page.CursorKey)
fc.Result = res
return ec.marshalOCursorKey2ᚖstring(ctx, field.Selections, res)
return ec.marshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_PageInfo_startCursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -2149,9 +2150,9 @@ func (ec *executionContext) _PageInfo_endCursor(ctx context.Context, field graph
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
res := resTmp.(*page.CursorKey)
fc.Result = res
return ec.marshalOCursorKey2ᚖstring(ctx, field.Selections, res)
return ec.marshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_PageInfo_endCursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -2585,9 +2586,9 @@ func (ec *executionContext) _TaskEdge_cursor(ctx context.Context, field graphql.
}
return graphql.Null
}
res := resTmp.(string)
res := resTmp.(page.CursorKey)
fc.Result = res
return ec.marshalNCursorKey2string(ctx, field.Selections, res)
return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_TaskEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -5358,13 +5359,13 @@ func (ec *executionContext) marshalNControlEdge2ᚖgithubᚗcomᚋgetproboᚋpro
return ec._ControlEdge(ctx, sel, v)
}
func (ec *executionContext) unmarshalNCursorKey2string(ctx context.Context, v any) (string, error) {
res, err := graphql.UnmarshalString(v)
func (ec *executionContext) unmarshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx context.Context, v any) (page.CursorKey, error) {
res, err := types.UnmarshalCursorKeyScalar(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNCursorKey2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
res := graphql.MarshalString(v)
func (ec *executionContext) marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx context.Context, sel ast.SelectionSet, v page.CursorKey) graphql.Marshaler {
res := types.MarshalCursorKeyScalar(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
@@ -5837,19 +5838,19 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
return res
}
func (ec *executionContext) unmarshalOCursorKey2ᚖstring(ctx context.Context, v any) (*string, error) {
func (ec *executionContext) unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx context.Context, v any) (*page.CursorKey, error) {
if v == nil {
return nil, nil
}
res, err := graphql.UnmarshalString(v)
res, err := types.UnmarshalCursorKeyScalar(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOCursorKey2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler {
func (ec *executionContext) marshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx context.Context, sel ast.SelectionSet, v *page.CursorKey) graphql.Marshaler {
if v == nil {
return graphql.Null
}
res := graphql.MarshalString(*v)
res := types.MarshalCursorKeyScalar(*v)
return res
}

View File

@@ -0,0 +1,55 @@
package types
import (
"errors"
"io"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/getprobo/probo/pkg/probo/coredata/page"
)
func NewCursor(
first *int,
after *page.CursorKey,
last *int,
before *page.CursorKey,
) *page.Cursor {
var (
size int
from *page.CursorKey
direction = page.Head
)
if first != nil {
size = *first
direction = page.Head
from = after
} else if last != nil {
size = *last
direction = page.Tail
from = before
}
return page.NewCursor(size, from, direction)
}
func MarshalCursorKeyScalar(ck page.CursorKey) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
w.Write([]byte(strconv.Quote(ck.String())))
})
}
func UnmarshalCursorKeyScalar(v interface{}) (page.CursorKey, error) {
s, ok := v.(string)
if !ok {
return page.CursorKeyNil, errors.New("must be a string")
}
ck, err := page.ParseCursorKey(s)
if err != nil {
return page.CursorKeyNil, err
}
return ck, nil
}

View File

@@ -0,0 +1,30 @@
package types
import (
"github.com/getprobo/probo/pkg/probo/coredata"
"github.com/getprobo/probo/pkg/probo/coredata/page"
)
func NewExecutionConnection(p *page.Page[*coredata.Framework]) *FrameworkConnection {
var edges = make([]*FrameworkEdge, len(p.Data))
for i := range edges {
edges[i] = NewFrameworkEdge(p.Data[i])
}
return &FrameworkConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
}
}
func NewFrameworkEdge(f *coredata.Framework) *FrameworkEdge {
return &FrameworkEdge{
Cursor: f.CursorKey(),
Node: NewExecution(f),
}
}
func NewExecution(e *coredata.Framework) *Framework {
return &Framework{}
}

View File

@@ -0,0 +1,25 @@
package types
import (
"gearno.de/ref"
"github.com/getprobo/probo/pkg/probo/coredata/page"
)
func NewPageInfo[T page.Paginable](p *page.Page[T]) *PageInfo {
var (
startCursor *page.CursorKey
endCursor *page.CursorKey
)
if len(p.Data) > 0 {
startCursor = ref.Ref(p.First().CursorKey())
endCursor = ref.Ref(p.Last().CursorKey())
}
return &PageInfo{
HasNextPage: p.Info.HasNext,
HasPreviousPage: p.Info.HasPrev,
StartCursor: startCursor,
EndCursor: endCursor,
}
}

View File

@@ -4,6 +4,8 @@ package types
import (
"time"
"github.com/getprobo/probo/pkg/probo/coredata/page"
)
type Node interface {
@@ -29,8 +31,8 @@ type ControlConnection struct {
}
type ControlEdge struct {
Cursor string `json:"cursor"`
Node *Control `json:"node"`
Cursor page.CursorKey `json:"cursor"`
Node *Control `json:"node"`
}
type Framework struct {
@@ -51,8 +53,8 @@ type FrameworkConnection struct {
}
type FrameworkEdge struct {
Cursor string `json:"cursor"`
Node *Framework `json:"node"`
Cursor page.CursorKey `json:"cursor"`
Node *Framework `json:"node"`
}
type Organization struct {
@@ -67,10 +69,10 @@ func (Organization) IsNode() {}
func (this Organization) GetID() string { return this.ID }
type PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor *string `json:"startCursor,omitempty"`
EndCursor *string `json:"endCursor,omitempty"`
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor *page.CursorKey `json:"startCursor,omitempty"`
EndCursor *page.CursorKey `json:"endCursor,omitempty"`
}
type Query struct {
@@ -92,6 +94,6 @@ type TaskConnection struct {
}
type TaskEdge struct {
Cursor string `json:"cursor"`
Node *Task `json:"node"`
Cursor page.CursorKey `json:"cursor"`
Node *Task `json:"node"`
}

View File

@@ -10,21 +10,29 @@ import (
"github.com/getprobo/probo/pkg/api/console/v1/schema"
"github.com/getprobo/probo/pkg/api/console/v1/types"
"github.com/getprobo/probo/pkg/probo/coredata/page"
)
// Tasks is the resolver for the tasks field.
func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *int, after *string, last *int, before *string) (*types.TaskConnection, error) {
func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error) {
panic(fmt.Errorf("not implemented: Tasks - tasks"))
}
// Controls is the resolver for the controls field.
func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, first *int, after *string, last *int, before *string) (*types.ControlConnection, error) {
func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlConnection, error) {
panic(fmt.Errorf("not implemented: Controls - controls"))
}
// Frameworks is the resolver for the frameworks field.
func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *string, last *int, before *string) (*types.FrameworkConnection, error) {
panic(fmt.Errorf("not implemented: Frameworks - frameworks"))
func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) {
cursor := types.NewCursor(first, after, last, before)
page, err := r.svc.ListOrganizationFramework(ctx, "", cursor)
if err != nil {
return nil, fmt.Errorf("cannot list organization frameworks: %w", err)
}
return types.NewExecutionConnection(page), nil
}
// Node is the resolver for the node field.

View File

@@ -2,31 +2,97 @@ package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/probo/coredata/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
)
type (
Framework struct {
ID string
ID uuid.UUID
OrganizationID string
ContentID string
Name string
Description string
ContentRef string
CreatedAt time.Time
UpdatedAt time.Time
}
Frameworks []*Framework
)
func (f *Framework) CursorKey() page.CursorKey {
return page.NewCursorKey(f.ID, f.CreatedAt)
}
func (f *Framework) scan(r pgx.Row) error {
return r.Scan(
&f.ID,
&f.OrganizationID,
&f.ContentID,
&f.Name,
&f.Description,
&f.ContentRef,
&f.CreatedAt,
&f.UpdatedAt,
)
}
func (f *Frameworks) LoadByOrganizationID(
ctx context.Context,
conn pg.Conn,
organizationID string,
cursor *page.Cursor,
) error {
q := `
SELECT
framework_id,
organization_id,
name,
description,
content_ref,
created_at,
updated_at
FROM
frameworks
WHERE
organization_id = @organization_id
AND %
ORDER BY name ASC;
`
q = fmt.Sprintf(q, cursor.SQLFragment())
args := pgx.NamedArgs{"organization_id": organizationID}
maps.Copy(args, cursor.SQLArguments())
r, err := conn.Query(ctx, q, args)
if err != nil {
return err
}
frameworks := Frameworks{}
for r.Next() {
framework := &Framework{}
if err := framework.scan(r); err != nil {
return err
}
frameworks = append(frameworks, framework)
}
defer r.Close()
if err = r.Err(); err != nil {
return err
}
*f = frameworks
return nil
}
func (f *Framework) LoadByID(
ctx context.Context,
@@ -37,7 +103,9 @@ func (f *Framework) LoadByID(
SELECT
framework_id,
organization_id,
content_id,
name,
description,
content_ref,
created_at,
updated_at
FROM

View File

@@ -0,0 +1,98 @@
package page
import (
"fmt"
"github.com/jackc/pgx/v5"
)
type (
Cursor struct {
Size int
Key *CursorKey
Position Position
}
Position int8
)
const (
DefaultCursorSize = 25
Tail Position = iota
Head
)
func (p Position) ToDirection() string {
switch p {
case Tail:
return "ASC"
case Head:
return "DESC"
default:
panic(fmt.Errorf("unknown direction: %d", p))
}
}
func NewCursor(size int, from *CursorKey, pos Position) *Cursor {
if size == 0 {
size = DefaultCursorSize
}
return &Cursor{
Size: size,
Key: from,
Position: pos,
}
}
func (c *Cursor) SQLFragment() string {
return `
CASE
WHEN @cursor_order = 'DESC' AND @cursor_from_id::TEXT IS NOT NULL THEN (
(created_at <= @cursor_from_ts) AND NOT (created_at = @cursor_from_ts AND id > @cursor_from_id)
)
WHEN @cursor_order = 'ASC' AND @cursor_from_id::TEXT IS NOT NULL THEN (
(created_at >= @cursor_from_ts) AND NOT (created_at = @cursor_from_ts AND id < @cursor_from_id)
)
ELSE TRUE
END
ORDER BY
CASE
WHEN @cursor_order = 'ASC' THEN created_at
END ASC,
CASE
WHEN @cursor_order = 'ASC' THEN id
END ASC,
CASE
WHEN @cursor_order = 'DESC' THEN created_at
END DESC,
CASE
WHEN @cursor_order = 'DESC' THEN id
END DESC
LIMIT @cursor_limit
`
}
func (c *Cursor) SQLArguments() pgx.NamedArgs {
var size = c.Size
if c.Key == nil {
size += 1
} else {
size += 2
}
arguments := pgx.NamedArgs{
"cursor_order": c.Position.ToDirection(),
"cursor_limit": size,
"cursor_from_id": nil,
"cursor_from_ts": nil,
}
if c.Key != nil {
arguments["cursor_from_id"] = c.Key.ID()
arguments["cursor_from_ts"] = c.Key.Timestamp()
}
return arguments
}

View File

@@ -0,0 +1,105 @@
package page
import (
"encoding/base64"
"encoding/binary"
"errors"
"time"
"go.gearno.de/crypto/uuid"
)
type (
CursorKey [byteLength]byte
)
var (
CursorKeyNil CursorKey
ErrInvalidFormat = errors.New("invalid format")
)
const (
byteLength = 28
)
func ParseCursorKey(s string) (CursorKey, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return CursorKeyNil, ErrInvalidFormat
}
ck, err := CursorKeyFromBytes(b)
if err != nil {
return CursorKeyNil, ErrInvalidFormat
}
return ck, nil
}
func CursorKeyFromBytes(b []byte) (CursorKey, error) {
var ck CursorKey
if len(b) != byteLength {
return CursorKeyNil, ErrInvalidFormat
}
copy(ck[:], b)
return ck, nil
}
func NewCursorKey(id uuid.UUID, t time.Time) CursorKey {
var cursorKey CursorKey
copy(cursorKey[:20], id[:])
_ = binary.PutVarint(cursorKey[20:], t.UnixMicro())
return cursorKey
}
func (ck CursorKey) Bytes() []byte {
return ck[:]
}
func (ck CursorKey) String() string {
return base64.RawURLEncoding.EncodeToString(ck.Bytes())
}
func (ck CursorKey) Timestamp() time.Time {
return ck.ID().Timestamp()
}
func (ck CursorKey) ID() uuid.UUID {
id, _ := uuid.FromBytes(ck[:])
return id
}
func (ck CursorKey) MarshalText() ([]byte, error) {
return []byte(ck.String()), nil
}
func (ck *CursorKey) UnmarshalText(data []byte) error {
ck2, err := ParseCursorKey(string(data))
if err != nil {
return err
}
*ck = ck2
return nil
}
func (ck CursorKey) MarshalBinary() ([]byte, error) {
return ck.Bytes(), nil
}
func (ck *CursorKey) UnmarshalBinary(b []byte) error {
ck2, err := CursorKeyFromBytes(b)
if err != nil {
return err
}
*ck = ck2
return nil
}

View File

@@ -0,0 +1,101 @@
package page
type (
Paginable interface {
CursorKey() CursorKey
}
PageInfo struct {
HasNext bool
HasPrev bool
}
Page[T Paginable] struct {
Info *PageInfo
Data []T
}
)
func (p *Page[T]) First() T {
if len(p.Data) == 0 {
var zero T
return zero
}
return p.Data[0]
}
func (p *Page[T]) Last() T {
if len(p.Data) == 0 {
var zero T
return zero
}
return p.Data[len(p.Data)-1]
}
func NewPage[T Paginable](data []T, c *Cursor) *Page[T] {
pi := &PageInfo{}
if len(data) == 0 {
return &Page[T]{
Info: pi,
Data: data,
}
}
edges := data
firstFromData := data[0]
switch c.Position {
case Head:
if c.Key != nil {
if len(edges) == c.Size+2 {
edges = edges[1 : len(edges)-1]
} else {
edges = edges[1:]
}
} else if c.Key == nil && len(edges) == c.Size+1 {
edges = edges[0 : len(edges)-1]
}
if c.Key != nil && *c.Key == firstFromData.CursorKey() {
pi.HasPrev = true
}
if c.Key != nil && c.Size+2 == len(data) {
pi.HasNext = true
} else if c.Key == nil && c.Size+1 == len(data) {
pi.HasNext = true
}
case Tail:
for i, j := 0, len(edges)-1; i < j; i, j = i+1, j-1 {
edges[i], edges[j] = edges[j], edges[i]
}
if c.Key != nil {
if len(edges) == c.Size+2 {
edges = edges[1 : len(edges)-1]
} else {
edges = edges[0 : len(edges)-1]
}
} else if c.Key == nil && len(edges) == c.Size+1 {
edges = edges[1:]
}
if c.Key != nil && *c.Key == firstFromData.CursorKey() {
pi.HasNext = true
}
if c.Key != nil && c.Size+2 == len(data) {
pi.HasPrev = true
} else if c.Key == nil && c.Size+1 == len(data) {
pi.HasPrev = true
}
}
return &Page[T]{
Info: pi,
Data: edges,
}
}

View File

@@ -3,6 +3,8 @@ package probo
import (
"context"
"github.com/getprobo/probo/pkg/probo/coredata"
"github.com/getprobo/probo/pkg/probo/coredata/page"
"go.gearno.de/kit/pg"
)
@@ -17,3 +19,44 @@ func NewService(ctx context.Context, pgClient *pg.Client) *Service {
pg: pgClient,
}
}
func (s *Service) GetOrganization(
ctx context.Context,
organizationID string,
) (*coredata.Organization, error) {
organization := &coredata.Organization{}
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return organization.LoadByID(ctx, conn, organizationID)
},
)
if err != nil {
return nil, err
}
return organization, nil
}
func (s *Service) ListOrganizationFramework(
ctx context.Context,
organizationID string,
cursor *page.Cursor,
) (*page.Page[*coredata.Framework], error) {
var frameworks coredata.Frameworks
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return frameworks.LoadByOrganizationID(ctx, conn, organizationID, cursor)
},
)
if err != nil {
return nil, err
}
return page.NewPage(frameworks, cursor), nil
}