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 go 1.23.4
require ( require (
gearno.de/ref v0.0.0-20221013162104-a522beda40f4
github.com/99designs/gqlgen v0.17.63 github.com/99designs/gqlgen v0.17.63
github.com/jackc/pgx/v5 v5.7.1 github.com/jackc/pgx/v5 v5.7.1
github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_golang v1.20.5
github.com/vektah/gqlparser/v2 v2.5.21 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.gearno.de/kit v0.0.0-20241119134016-ee4f36699e6b
go.opentelemetry.io/otel/trace v1.32.0 go.opentelemetry.io/otel/trace v1.32.0
) )
@@ -34,7 +36,6 @@ require (
github.com/sosodev/duration v1.3.1 // indirect github.com/sosodev/duration v1.3.1 // indirect
github.com/urfave/cli/v2 v2.27.5 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // 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.gearno.de/x/panicf v0.1.1 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace 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 h1:HCdaYDPd9HqUXRchEvmE3EFzELRwLlaJ8DBuyC8Cqto=
github.com/99designs/gqlgen v0.17.63/go.mod h1:sVCM2iwIZisJjTI/DEC3fpH+HFgxY1496ZJ+jbT9IjA= github.com/99designs/gqlgen v0.17.63/go.mod h1:sVCM2iwIZisJjTI/DEC3fpH+HFgxY1496ZJ+jbT9IjA=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=

View File

@@ -22,3 +22,6 @@ models:
Datetime: Datetime:
model: model:
- "github.com/99designs/gqlgen/graphql.Time" - "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 package console_v1
import "github.com/getprobo/probo/pkg/probo"
type ( 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"
"github.com/99designs/gqlgen/graphql/introspection" "github.com/99designs/gqlgen/graphql/introspection"
"github.com/getprobo/probo/pkg/api/console/v1/types" "github.com/getprobo/probo/pkg/api/console/v1/types"
"github.com/getprobo/probo/pkg/probo/coredata/page"
gqlparser "github.com/vektah/gqlparser/v2" gqlparser "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/ast"
) )
@@ -54,7 +55,7 @@ type ComplexityRoot struct {
Description func(childComplexity int) int Description func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Name 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 UpdatedAt func(childComplexity int) int
} }
@@ -69,7 +70,7 @@ type ComplexityRoot struct {
} }
Framework 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 CreatedAt func(childComplexity int) int
Description func(childComplexity int) int Description func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
@@ -89,7 +90,7 @@ type ComplexityRoot struct {
Organization struct { Organization struct {
CreatedAt func(childComplexity int) int 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 ID func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
@@ -125,13 +126,13 @@ type ComplexityRoot struct {
} }
type ControlResolver interface { 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 { 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 { 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 { type QueryResolver interface {
Node(ctx context.Context, id string) (types.Node, error) 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 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": case "Control.updatedAt":
if e.complexity.Control.UpdatedAt == nil { if e.complexity.Control.UpdatedAt == nil {
@@ -241,7 +242,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false 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": case "Framework.createdAt":
if e.complexity.Framework.CreatedAt == nil { if e.complexity.Framework.CreatedAt == nil {
@@ -323,7 +324,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false 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": case "Organization.id":
if e.complexity.Organization.ID == nil { if e.complexity.Organization.ID == nil {
@@ -688,13 +689,13 @@ func (ec *executionContext) field_Control_tasks_argsFirst(
func (ec *executionContext) field_Control_tasks_argsAfter( func (ec *executionContext) field_Control_tasks_argsAfter(
ctx context.Context, ctx context.Context,
rawArgs map[string]any, rawArgs map[string]any,
) (*string, error) { ) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
if tmp, ok := rawArgs["after"]; ok { 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 return zeroVal, nil
} }
@@ -714,13 +715,13 @@ func (ec *executionContext) field_Control_tasks_argsLast(
func (ec *executionContext) field_Control_tasks_argsBefore( func (ec *executionContext) field_Control_tasks_argsBefore(
ctx context.Context, ctx context.Context,
rawArgs map[string]any, rawArgs map[string]any,
) (*string, error) { ) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
if tmp, ok := rawArgs["before"]; ok { 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 return zeroVal, nil
} }
@@ -765,13 +766,13 @@ func (ec *executionContext) field_Framework_controls_argsFirst(
func (ec *executionContext) field_Framework_controls_argsAfter( func (ec *executionContext) field_Framework_controls_argsAfter(
ctx context.Context, ctx context.Context,
rawArgs map[string]any, rawArgs map[string]any,
) (*string, error) { ) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
if tmp, ok := rawArgs["after"]; ok { 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 return zeroVal, nil
} }
@@ -791,13 +792,13 @@ func (ec *executionContext) field_Framework_controls_argsLast(
func (ec *executionContext) field_Framework_controls_argsBefore( func (ec *executionContext) field_Framework_controls_argsBefore(
ctx context.Context, ctx context.Context,
rawArgs map[string]any, rawArgs map[string]any,
) (*string, error) { ) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
if tmp, ok := rawArgs["before"]; ok { 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 return zeroVal, nil
} }
@@ -842,13 +843,13 @@ func (ec *executionContext) field_Organization_frameworks_argsFirst(
func (ec *executionContext) field_Organization_frameworks_argsAfter( func (ec *executionContext) field_Organization_frameworks_argsAfter(
ctx context.Context, ctx context.Context,
rawArgs map[string]any, rawArgs map[string]any,
) (*string, error) { ) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after"))
if tmp, ok := rawArgs["after"]; ok { 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 return zeroVal, nil
} }
@@ -868,13 +869,13 @@ func (ec *executionContext) field_Organization_frameworks_argsLast(
func (ec *executionContext) field_Organization_frameworks_argsBefore( func (ec *executionContext) field_Organization_frameworks_argsBefore(
ctx context.Context, ctx context.Context,
rawArgs map[string]any, rawArgs map[string]any,
) (*string, error) { ) (*page.CursorKey, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before"))
if tmp, ok := rawArgs["before"]; ok { 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 return zeroVal, nil
} }
@@ -1100,7 +1101,7 @@ func (ec *executionContext) _Control_tasks(ctx context.Context, field graphql.Co
ctx = graphql.WithFieldContext(ctx, fc) ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children 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 { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -1329,9 +1330,9 @@ func (ec *executionContext) _ControlEdge_cursor(ctx context.Context, field graph
} }
return graphql.Null return graphql.Null
} }
res := resTmp.(string) res := resTmp.(page.CursorKey)
fc.Result = res 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) { 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) ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children 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 { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -1750,9 +1751,9 @@ func (ec *executionContext) _FrameworkEdge_cursor(ctx context.Context, field gra
} }
return graphql.Null return graphql.Null
} }
res := resTmp.(string) res := resTmp.(page.CursorKey)
fc.Result = res 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) { 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) ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children 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 { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -2114,9 +2115,9 @@ func (ec *executionContext) _PageInfo_startCursor(ctx context.Context, field gra
if resTmp == nil { if resTmp == nil {
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(*page.CursorKey)
fc.Result = res 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) { 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 { if resTmp == nil {
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(*page.CursorKey)
fc.Result = res 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) { 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 return graphql.Null
} }
res := resTmp.(string) res := resTmp.(page.CursorKey)
fc.Result = res 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) { 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) return ec._ControlEdge(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNCursorKey2string(ctx context.Context, v any) (string, error) { func (ec *executionContext) unmarshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx context.Context, v any) (page.CursorKey, error) {
res, err := graphql.UnmarshalString(v) res, err := types.UnmarshalCursorKeyScalar(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
} }
func (ec *executionContext) marshalNCursorKey2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { func (ec *executionContext) marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx context.Context, sel ast.SelectionSet, v page.CursorKey) graphql.Marshaler {
res := graphql.MarshalString(v) res := types.MarshalCursorKeyScalar(v)
if res == graphql.Null { if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow") 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 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 { if v == nil {
return nil, nil return nil, nil
} }
res, err := graphql.UnmarshalString(v) res, err := types.UnmarshalCursorKeyScalar(v)
return &res, graphql.ErrorOnPath(ctx, err) 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 { if v == nil {
return graphql.Null return graphql.Null
} }
res := graphql.MarshalString(*v) res := types.MarshalCursorKeyScalar(*v)
return res 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 ( import (
"time" "time"
"github.com/getprobo/probo/pkg/probo/coredata/page"
) )
type Node interface { type Node interface {
@@ -29,8 +31,8 @@ type ControlConnection struct {
} }
type ControlEdge struct { type ControlEdge struct {
Cursor string `json:"cursor"` Cursor page.CursorKey `json:"cursor"`
Node *Control `json:"node"` Node *Control `json:"node"`
} }
type Framework struct { type Framework struct {
@@ -51,8 +53,8 @@ type FrameworkConnection struct {
} }
type FrameworkEdge struct { type FrameworkEdge struct {
Cursor string `json:"cursor"` Cursor page.CursorKey `json:"cursor"`
Node *Framework `json:"node"` Node *Framework `json:"node"`
} }
type Organization struct { type Organization struct {
@@ -67,10 +69,10 @@ func (Organization) IsNode() {}
func (this Organization) GetID() string { return this.ID } func (this Organization) GetID() string { return this.ID }
type PageInfo struct { type PageInfo struct {
HasNextPage bool `json:"hasNextPage"` HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"` HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor *string `json:"startCursor,omitempty"` StartCursor *page.CursorKey `json:"startCursor,omitempty"`
EndCursor *string `json:"endCursor,omitempty"` EndCursor *page.CursorKey `json:"endCursor,omitempty"`
} }
type Query struct { type Query struct {
@@ -92,6 +94,6 @@ type TaskConnection struct {
} }
type TaskEdge struct { type TaskEdge struct {
Cursor string `json:"cursor"` Cursor page.CursorKey `json:"cursor"`
Node *Task `json:"node"` 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/schema"
"github.com/getprobo/probo/pkg/api/console/v1/types" "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. // 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")) panic(fmt.Errorf("not implemented: Tasks - tasks"))
} }
// Controls is the resolver for the controls field. // 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")) panic(fmt.Errorf("not implemented: Controls - controls"))
} }
// Frameworks is the resolver for the frameworks field. // 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) { func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) {
panic(fmt.Errorf("not implemented: Frameworks - frameworks")) 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. // Node is the resolver for the node field.

View File

@@ -2,31 +2,97 @@ package coredata
import ( import (
"context" "context"
"fmt"
"maps"
"time" "time"
"github.com/getprobo/probo/pkg/probo/coredata/page"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
type ( type (
Framework struct { Framework struct {
ID string ID uuid.UUID
OrganizationID string OrganizationID string
ContentID string Name string
Description string
ContentRef string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt 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 { func (f *Framework) scan(r pgx.Row) error {
return r.Scan( return r.Scan(
&f.ID, &f.ID,
&f.OrganizationID, &f.OrganizationID,
&f.ContentID, &f.Name,
&f.Description,
&f.ContentRef,
&f.CreatedAt, &f.CreatedAt,
&f.UpdatedAt, &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( func (f *Framework) LoadByID(
ctx context.Context, ctx context.Context,
@@ -37,7 +103,9 @@ func (f *Framework) LoadByID(
SELECT SELECT
framework_id, framework_id,
organization_id, organization_id,
content_id, name,
description,
content_ref,
created_at, created_at,
updated_at updated_at
FROM 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 ( import (
"context" "context"
"github.com/getprobo/probo/pkg/probo/coredata"
"github.com/getprobo/probo/pkg/probo/coredata/page"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
@@ -17,3 +19,44 @@ func NewService(ctx context.Context, pgClient *pg.Client) *Service {
pg: pgClient, 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
}