diff --git a/pkg/api/console/v1/schema.graphql b/pkg/api/console/v1/schema.graphql index 065c9f447..349af7d08 100644 --- a/pkg/api/console/v1/schema.graphql +++ b/pkg/api/console/v1/schema.graphql @@ -7,6 +7,7 @@ directive @goField( scalar CursorKey scalar Void scalar Datetime +scalar Upload interface Node { id: ID! @@ -74,6 +75,7 @@ type Control implements Node { id: ID! name: String! description: String! + state: String! tasks( first: Int diff --git a/pkg/api/console/v1/schema/schema.go b/pkg/api/console/v1/schema/schema.go index a5dbde648..2c6198b37 100644 --- a/pkg/api/console/v1/schema/schema.go +++ b/pkg/api/console/v1/schema/schema.go @@ -56,6 +56,7 @@ type ComplexityRoot struct { Description func(childComplexity int) int ID func(childComplexity int) int Name func(childComplexity int) int + State func(childComplexity int) int Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int UpdatedAt func(childComplexity int) int } @@ -186,6 +187,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Control.Name(childComplexity), true + case "Control.state": + if e.complexity.Control.State == nil { + break + } + + return e.complexity.Control.State(childComplexity), true + case "Control.tasks": if e.complexity.Control.Tasks == nil { break @@ -542,6 +550,7 @@ var sources = []*ast.Source{ scalar CursorKey scalar Void scalar Datetime +scalar Upload interface Node { id: ID! @@ -609,6 +618,7 @@ type Control implements Node { id: ID! name: String! description: String! + state: String! tasks( first: Int @@ -1094,6 +1104,44 @@ func (ec *executionContext) fieldContext_Control_description(_ context.Context, return fc, nil } +func (ec *executionContext) _Control_state(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Control_state(ctx, field) + if err != nil { + return graphql.Null + } + 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 obj.State, nil + }) + 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Control_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Control", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Control_tasks(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Control_tasks(ctx, field) if err != nil { @@ -1388,6 +1436,8 @@ func (ec *executionContext) fieldContext_ControlEdge_node(_ context.Context, fie return ec.fieldContext_Control_name(ctx, field) case "description": return ec.fieldContext_Control_description(ctx, field) + case "state": + return ec.fieldContext_Control_state(ctx, field) case "tasks": return ec.fieldContext_Control_tasks(ctx, field) case "createdAt": @@ -4275,6 +4325,11 @@ func (ec *executionContext) _Control(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "state": + out.Values[i] = ec._Control_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "tasks": field := field diff --git a/pkg/api/console/v1/types/control.go b/pkg/api/console/v1/types/control.go index 5e8bd22c7..b7fc06810 100644 --- a/pkg/api/console/v1/types/control.go +++ b/pkg/api/console/v1/types/control.go @@ -44,6 +44,7 @@ func NewControl(c *coredata.Control) *Control { ID: c.ID, Name: c.Name, Description: c.Description, + State: string(c.State), CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt, } diff --git a/pkg/api/console/v1/types/types.go b/pkg/api/console/v1/types/types.go index 317a918b0..9a50fff1b 100644 --- a/pkg/api/console/v1/types/types.go +++ b/pkg/api/console/v1/types/types.go @@ -18,6 +18,7 @@ type Control struct { ID gid.GID `json:"id"` Name string `json:"name"` Description string `json:"description"` + State string `json:"state"` Tasks *TaskConnection `json:"tasks"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` diff --git a/pkg/api/console/v1/v1_resolver.go b/pkg/api/console/v1/v1_resolver.go index 64e440f05..e95c0bbbf 100644 --- a/pkg/api/console/v1/v1_resolver.go +++ b/pkg/api/console/v1/v1_resolver.go @@ -54,7 +54,6 @@ func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organi // Node is the resolver for the node field. func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) { - switch id.EntityType() { case coredata.OrganizationEntityType: organization, err := r.svc.GetOrganization(ctx, id) diff --git a/pkg/probo/coredata/control.go b/pkg/probo/coredata/control.go index e327773d5..3d5d1ff16 100644 --- a/pkg/probo/coredata/control.go +++ b/pkg/probo/coredata/control.go @@ -34,6 +34,7 @@ type ( FrameworkID string Name string Description string + State ControlState ContentRef string CreatedAt time.Time UpdatedAt time.Time @@ -52,6 +53,7 @@ func (c *Control) scan(r pgx.Row) error { &c.FrameworkID, &c.Name, &c.Description, + &c.State, &c.ContentRef, &c.CreatedAt, &c.UpdatedAt, @@ -66,19 +68,34 @@ func (c *Controls) LoadByFrameworkID( cursor *page.Cursor, ) error { q := ` +WITH control_states AS ( + SELECT + control_id, + to_state, + reason, + RANK() OVER w + FROM + control_state_transitions + WINDOW + w AS (PARTITION BY control_id ORDER BY created_at DESC) +) SELECT id, framework_id, name, description, + cs.to_state AS state, content_ref, created_at, updated_at FROM controls +INNER JOIN + control_states cs ON cs.control_id = controls.id WHERE %s AND framework_id = @framework_id + AND cs.rank = 1 AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) diff --git a/pkg/probo/coredata/migrations/20250126T141600Z.sql b/pkg/probo/coredata/migrations/20250126T141600Z.sql new file mode 100644 index 000000000..30f0e8299 --- /dev/null +++ b/pkg/probo/coredata/migrations/20250126T141600Z.sql @@ -0,0 +1,16 @@ +CREATE TYPE control_state AS ENUM ( + 'NOT_STARTED', + 'IN_PROGRESS', + 'NOT_APPLICABLE', + 'IMPLEMENTED', +); + +CREATE TABLE control_state_transitions ( + id TEXT PRIMARY KEY, + control_id TEXT REFERENCES controls(id) NOT NULL, + from_state control_state, + to_state control_state NOT NULL, + reason TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL +);