diff --git a/apps/trust/src/queries/TrustGraph.ts b/apps/trust/src/queries/TrustGraph.ts index db9eded1d..391416fa8 100644 --- a/apps/trust/src/queries/TrustGraph.ts +++ b/apps/trust/src/queries/TrustGraph.ts @@ -23,6 +23,9 @@ export const currentTrustGraphQuery = graphql` headquarterAddress } ...OverviewPageFragment + vendors(first: 0) { + totalCount + } audits(first: 50) { edges { node { diff --git a/pkg/coredata/vendor.go b/pkg/coredata/vendor.go index cde5cc255..9662c761a 100644 --- a/pkg/coredata/vendor.go +++ b/pkg/coredata/vendor.go @@ -283,6 +283,7 @@ func (v *Vendors) CountByOrganizationID( conn pg.Conn, scope Scoper, organizationID gid.GID, + filter *VendorFilter, ) (int, error) { q := ` SELECT @@ -292,12 +293,14 @@ FROM WHERE %s AND organization_id = @organization_id + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) diff --git a/pkg/probo/vendor_service.go b/pkg/probo/vendor_service.go index e67bdacc2..4c383aa2c 100644 --- a/pkg/probo/vendor_service.go +++ b/pkg/probo/vendor_service.go @@ -163,7 +163,8 @@ func (s VendorService) CountForOrganizationID( ctx, func(conn pg.Conn) (err error) { vendors := coredata.Vendors{} - count, err = vendors.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + filter := &coredata.VendorFilter{} + count, err = vendors.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count vendors: %w", err) } diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index 42f0cc6c3..5abf5a672 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -437,9 +437,13 @@ type Vendor implements Node { countries: [CountryCode!]! } -type VendorConnection { +type VendorConnection + @goModel( + model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.VendorConnection" + ) { edges: [VendorEdge!]! pageInfo: PageInfo! + totalCount: Int! @goField(forceResolver: true) } type VendorEdge { diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index 8ea2cea5a..d69afeeeb 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -57,6 +57,7 @@ type ResolverRoot interface { TrustCenter() TrustCenterResolver TrustCenterFile() TrustCenterFileResolver TrustCenterReference() TrustCenterReferenceResolver + VendorConnection() VendorConnectionResolver } type DirectiveRoot struct { @@ -251,8 +252,9 @@ type ComplexityRoot struct { } VendorConnection 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 } VendorEdge struct { @@ -319,6 +321,9 @@ type TrustCenterFileResolver interface { type TrustCenterReferenceResolver interface { LogoURL(ctx context.Context, obj *types.TrustCenterReference) (string, error) } +type VendorConnectionResolver interface { + TotalCount(ctx context.Context, obj *types.VendorConnection) (int, error) +} type executableSchema struct { schema *ast.Schema @@ -1066,6 +1071,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.VendorConnection.PageInfo(childComplexity), true + case "VendorConnection.totalCount": + if e.complexity.VendorConnection.TotalCount == nil { + break + } + + return e.complexity.VendorConnection.TotalCount(childComplexity), true case "VendorEdge.cursor": if e.complexity.VendorEdge.Cursor == nil { @@ -1640,9 +1651,13 @@ type Vendor implements Node { countries: [CountryCode!]! } -type VendorConnection { +type VendorConnection + @goModel( + model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.VendorConnection" + ) { edges: [VendorEdge!]! pageInfo: PageInfo! + totalCount: Int! @goField(forceResolver: true) } type VendorEdge { @@ -4890,6 +4905,8 @@ func (ec *executionContext) fieldContext_TrustCenter_vendors(ctx context.Context return ec.fieldContext_VendorConnection_edges(ctx, field) case "pageInfo": return ec.fieldContext_VendorConnection_pageInfo(ctx, field) + case "totalCount": + return ec.fieldContext_VendorConnection_totalCount(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type VendorConnection", field.Name) }, @@ -5944,7 +5961,7 @@ func (ec *executionContext) _VendorConnection_pageInfo(ctx context.Context, fiel return obj.PageInfo, nil }, nil, - ec.marshalNPageInfo2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐPageInfo, + ec.marshalNPageInfo2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐPageInfo, true, true, ) @@ -5973,6 +5990,35 @@ func (ec *executionContext) fieldContext_VendorConnection_pageInfo(_ context.Con return fc, nil } +func (ec *executionContext) _VendorConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.VendorConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_VendorConnection_totalCount, + func(ctx context.Context) (any, error) { + return ec.resolvers.VendorConnection().TotalCount(ctx, obj) + }, + nil, + ec.marshalNInt2int, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_VendorConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VendorConnection", + 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) _VendorEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.VendorEdge) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -10019,13 +10065,49 @@ func (ec *executionContext) _VendorConnection(ctx context.Context, sel ast.Selec case "edges": out.Values[i] = ec._VendorConnection_edges(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "pageInfo": out.Values[i] = ec._VendorConnection_pageInfo(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } + 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._VendorConnection_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) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -11392,6 +11474,22 @@ func (ec *executionContext) marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGI return res } +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + _ = sel + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) marshalNNode2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐNode(ctx context.Context, sel ast.SelectionSet, v types.Node) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -11416,6 +11514,10 @@ func (ec *executionContext) marshalNOrganization2ᚖgoᚗproboᚗincᚋproboᚋp return ec._Organization(ctx, sel, v) } +func (ec *executionContext) marshalNPageInfo2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐPageInfo(ctx context.Context, sel ast.SelectionSet, v types.PageInfo) graphql.Marshaler { + return ec._PageInfo(ctx, sel, &v) +} + func (ec *executionContext) marshalNPageInfo2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐPageInfo(ctx context.Context, sel ast.SelectionSet, v *types.PageInfo) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index 97622aaa6..fe644a497 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -254,11 +254,6 @@ type Vendor struct { func (Vendor) IsNode() {} func (this Vendor) GetID() gid.GID { return this.ID } -type VendorConnection struct { - Edges []*VendorEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` -} - type VendorEdge struct { Cursor page.CursorKey `json:"cursor"` Node *Vendor `json:"node"` diff --git a/pkg/server/api/trust/v1/types/vendor.go b/pkg/server/api/trust/v1/types/vendor.go index 016c1e7dc..fdc94bfe9 100644 --- a/pkg/server/api/trust/v1/types/vendor.go +++ b/pkg/server/api/trust/v1/types/vendor.go @@ -16,11 +16,25 @@ package types import ( "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" ) +type ( + VendorConnection struct { + TotalCount int + Edges []*VendorEdge + PageInfo PageInfo + + Resolver any + ParentID gid.GID + } +) + func NewVendorConnection( p *page.Page[*coredata.Vendor, coredata.VendorOrderField], + parentType any, + parentID gid.GID, ) *VendorConnection { edges := make([]*VendorEdge, len(p.Data)) for i, vendor := range p.Data { @@ -29,7 +43,10 @@ func NewVendorConnection( return &VendorConnection{ Edges: edges, - PageInfo: NewPageInfo(p), + PageInfo: *NewPageInfo(p), + + Resolver: parentType, + ParentID: parentID, } } diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 6e191eb90..b82b9420f 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -954,7 +954,7 @@ func (r *trustCenterResolver) Vendors(ctx context.Context, obj *types.TrustCente return nil, gqlutils.Internal(ctx) } - return types.NewVendorConnection(vendorPage), nil + return types.NewVendorConnection(vendorPage, r, obj.ID), nil } // References is the resolver for the references field. @@ -1075,6 +1075,22 @@ func (r *trustCenterReferenceResolver) LogoURL(ctx context.Context, obj *types.T return logoURL, nil } +// TotalCount is the resolver for the totalCount field. +func (r *vendorConnectionResolver) TotalCount(ctx context.Context, obj *types.VendorConnection) (int, error) { + trustService := r.TrustService(ctx, obj.ParentID.TenantID()) + + switch obj.Resolver.(type) { + case *trustCenterResolver: + count, err := trustService.Vendors.CountForTrustCenterId(ctx, obj.ParentID) + if err != nil { + panic(fmt.Errorf("cannot count vendors: %w", err)) + } + return count, nil + } + + panic(fmt.Errorf("not implemented: TotalCount for parent type %T", obj.Resolver)) +} + // Audit returns schema.AuditResolver implementation. func (r *Resolver) Audit() schema.AuditResolver { return &auditResolver{r} } @@ -1109,6 +1125,11 @@ func (r *Resolver) TrustCenterReference() schema.TrustCenterReferenceResolver { return &trustCenterReferenceResolver{r} } +// VendorConnection returns schema.VendorConnectionResolver implementation. +func (r *Resolver) VendorConnection() schema.VendorConnectionResolver { + return &vendorConnectionResolver{r} +} + type auditResolver struct{ *Resolver } type documentResolver struct{ *Resolver } type frameworkResolver struct{ *Resolver } @@ -1119,3 +1140,4 @@ type reportResolver struct{ *Resolver } type trustCenterResolver struct{ *Resolver } type trustCenterFileResolver struct{ *Resolver } type trustCenterReferenceResolver struct{ *Resolver } +type vendorConnectionResolver struct{ *Resolver } diff --git a/pkg/trust/vendor_service.go b/pkg/trust/vendor_service.go index 0a563d035..d544f29d8 100644 --- a/pkg/trust/vendor_service.go +++ b/pkg/trust/vendor_service.go @@ -18,10 +18,10 @@ import ( "context" "fmt" + "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" - "go.gearno.de/kit/pg" ) type VendorService struct { @@ -82,3 +82,37 @@ func (s VendorService) ListForOrganizationId( return page.NewPage(vendors, cursor), nil } + +func (s VendorService) CountForTrustCenterId( + ctx context.Context, + trustCenterID gid.GID, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + trustCenter, _, err := s.svc.TrustCenters.Get(ctx, trustCenterID) + if err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + vendors := &coredata.Vendors{} + showOnTrustCenter := true + var nilSnapshotID *gid.GID = nil + filter := coredata.NewVendorFilter(&nilSnapshotID, &showOnTrustCenter) + count, err = vendors.CountByOrganizationID(ctx, conn, s.svc.scope, trustCenter.OrganizationID, filter) + if err != nil { + return fmt.Errorf("cannot count vendors: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, err + } + + return count, nil +}