diff --git a/pkg/coredata/framework.go b/pkg/coredata/framework.go index 00223e693..f2b16e102 100644 --- a/pkg/coredata/framework.go +++ b/pkg/coredata/framework.go @@ -60,6 +60,37 @@ func (f *Framework) CursorKey(orderBy FrameworkOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (f *Frameworks) CountByOrganizationID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, +) (int, error) { + q := ` +SELECT + COUNT(id) +FROM + frameworks +WHERE + %s + AND organization_id = @organization_id +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.NamedArgs{"organization_id": organizationID} + maps.Copy(args, scope.SQLArguments()) + + row := conn.QueryRow(ctx, q, args) + + var count int + if err := row.Scan(&count); err != nil { + return 0, fmt.Errorf("cannot scan count: %w", err) + } + + return count, nil +} + func (f *Frameworks) LoadByOrganizationID( ctx context.Context, conn pg.Conn, diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 7305ed4a1..f1dc1f0c1 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -104,6 +104,32 @@ func (s FrameworkService) Create( return framework, nil } +func (s FrameworkService) CountForOrganizationID( + ctx context.Context, + organizationID gid.GID, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + frameworks := &coredata.Frameworks{} + count, err = frameworks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + if err != nil { + return fmt.Errorf("cannot count frameworks: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, fmt.Errorf("cannot count frameworks: %w", err) + } + + return count, nil +} + func (s FrameworkService) ListForOrganizationID( ctx context.Context, organizationID gid.GID, diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 40ba2d11d..0a60591e7 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1090,7 +1090,11 @@ type VendorEdge { node: Vendor! } -type FrameworkConnection { +type FrameworkConnection + @goModel( + model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.FrameworkConnection" + ) { + totalCount: Int! @goField(forceResolver: true) edges: [FrameworkEdge!]! pageInfo: PageInfo! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 348b6e9e4..96f993cb1 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -51,6 +51,7 @@ type ResolverRoot interface { DocumentVersionSignature() DocumentVersionSignatureResolver Evidence() EvidenceResolver Framework() FrameworkResolver + FrameworkConnection() FrameworkConnectionResolver Measure() MeasureResolver Mutation() MutationResolver Organization() OrganizationResolver @@ -424,8 +425,9 @@ type ComplexityRoot struct { } FrameworkConnection struct { - Edges func(childComplexity int) int - PageInfo func(childComplexity int) int + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + TotalCount func(childComplexity int) int } FrameworkEdge struct { @@ -914,6 +916,9 @@ type FrameworkResolver interface { Organization(ctx context.Context, obj *types.Framework) (*types.Organization, error) Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) } +type FrameworkConnectionResolver interface { + TotalCount(ctx context.Context, obj *types.FrameworkConnection) (int, error) +} type MeasureResolver interface { Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) Tasks(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error) @@ -2276,6 +2281,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.FrameworkConnection.PageInfo(childComplexity), true + case "FrameworkConnection.totalCount": + if e.complexity.FrameworkConnection.TotalCount == nil { + break + } + + return e.complexity.FrameworkConnection.TotalCount(childComplexity), true + case "FrameworkEdge.cursor": if e.complexity.FrameworkEdge.Cursor == nil { break @@ -5799,7 +5811,11 @@ type VendorEdge { node: Vendor! } -type FrameworkConnection { +type FrameworkConnection + @goModel( + model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.FrameworkConnection" + ) { + totalCount: Int! @goField(forceResolver: true) edges: [FrameworkEdge!]! pageInfo: PageInfo! } @@ -20110,6 +20126,50 @@ func (ec *executionContext) fieldContext_Framework_updatedAt(_ context.Context, return fc, nil } +func (ec *executionContext) _FrameworkConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.FrameworkConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_FrameworkConnection_totalCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.FrameworkConnection().TotalCount(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_FrameworkConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "FrameworkConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _FrameworkConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.FrameworkConnection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_FrameworkConnection_edges(ctx, field) if err != nil { @@ -20186,9 +20246,9 @@ func (ec *executionContext) _FrameworkConnection_pageInfo(ctx context.Context, f } return graphql.Null } - res := resTmp.(*types.PageInfo) + res := resTmp.(types.PageInfo) fc.Result = res - return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) + return ec.marshalNPageInfo2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_FrameworkConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -25353,6 +25413,8 @@ func (ec *executionContext) fieldContext_Organization_frameworks(ctx context.Con IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { + case "totalCount": + return ec.fieldContext_FrameworkConnection_totalCount(ctx, field) case "edges": return ec.fieldContext_FrameworkConnection_edges(ctx, field) case "pageInfo": @@ -43773,15 +43835,51 @@ func (ec *executionContext) _FrameworkConnection(ctx context.Context, sel ast.Se switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("FrameworkConnection") + case "totalCount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._FrameworkConnection_totalCount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "edges": out.Values[i] = ec._FrameworkConnection_edges(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "pageInfo": out.Values[i] = ec._FrameworkConnection_pageInfo(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } default: panic("unknown field " + strconv.Quote(field.Name)) diff --git a/pkg/server/api/console/v1/types/framework.go b/pkg/server/api/console/v1/types/framework.go index 9c54eff9c..982caa97e 100644 --- a/pkg/server/api/console/v1/types/framework.go +++ b/pkg/server/api/console/v1/types/framework.go @@ -16,14 +16,28 @@ package types import ( "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" ) type ( FrameworkOrderBy OrderBy[coredata.FrameworkOrderField] + + FrameworkConnection struct { + TotalCount int + Edges []*FrameworkEdge + PageInfo PageInfo + + Resolver any + ParentID gid.GID + } ) -func NewFrameworkConnection(p *page.Page[*coredata.Framework, coredata.FrameworkOrderField]) *FrameworkConnection { +func NewFrameworkConnection( + p *page.Page[*coredata.Framework, coredata.FrameworkOrderField], + parentType any, + parentID gid.GID, +) *FrameworkConnection { var edges = make([]*FrameworkEdge, len(p.Data)) for i := range edges { @@ -32,7 +46,10 @@ func NewFrameworkConnection(p *page.Page[*coredata.Framework, coredata.Framework return &FrameworkConnection{ Edges: edges, - PageInfo: NewPageInfo(p), + PageInfo: *NewPageInfo(p), + + Resolver: parentType, + ParentID: parentID, } } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 3b2e38e07..7fc9f91c8 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -660,11 +660,6 @@ type Framework struct { func (Framework) IsNode() {} func (this Framework) GetID() gid.GID { return this.ID } -type FrameworkConnection struct { - Edges []*FrameworkEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` -} - type FrameworkEdge struct { Cursor page.CursorKey `json:"cursor"` Node *Framework `json:"node"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 320c6adbe..6cf84893e 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -202,9 +202,9 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C return 0, fmt.Errorf("cannot count controls: %w", err) } return count, nil - default: - panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) } + + panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) } // Owner is the resolver for the owner field. @@ -584,6 +584,22 @@ func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, return types.NewControlConnection(page, r, obj.ID, controlFilter), nil } +// TotalCount is the resolver for the totalCount field. +func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types.FrameworkConnection) (int, error) { + svc := GetTenantService(ctx, r.proboSvc, obj.ParentID.TenantID()) + + switch obj.Resolver.(type) { + case *organizationResolver: + count, err := svc.Frameworks.CountForOrganizationID(ctx, obj.ParentID) + if err != nil { + return 0, fmt.Errorf("cannot count frameworks: %w", err) + } + return count, nil + } + + panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) +} + // Evidences is the resolver for the evidences field. func (r *measureResolver) Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) { svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID()) @@ -2026,7 +2042,7 @@ func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organi panic(fmt.Errorf("cannot list organization frameworks: %w", err)) } - return types.NewFrameworkConnection(page), nil + return types.NewFrameworkConnection(page, r, obj.ID), nil } // Controls is the resolver for the controls field. @@ -2832,6 +2848,11 @@ func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver // Framework returns schema.FrameworkResolver implementation. func (r *Resolver) Framework() schema.FrameworkResolver { return &frameworkResolver{r} } +// FrameworkConnection returns schema.FrameworkConnectionResolver implementation. +func (r *Resolver) FrameworkConnection() schema.FrameworkConnectionResolver { + return &frameworkConnectionResolver{r} +} + // Measure returns schema.MeasureResolver implementation. func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} } @@ -2878,6 +2899,7 @@ type documentVersionResolver struct{ *Resolver } type documentVersionSignatureResolver struct{ *Resolver } type evidenceResolver struct{ *Resolver } type frameworkResolver struct{ *Resolver } +type frameworkConnectionResolver struct{ *Resolver } type measureResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type organizationResolver struct{ *Resolver }