From 9a191d7ee1271dbf3402a840aa6a943e0e9d8d78 Mon Sep 17 00:00:00 2001 From: gearnode Date: Mon, 27 Jan 2025 19:02:10 +0100 Subject: [PATCH] Add control state transitions graphql resolver Signed-off-by: gearnode --- pkg/api/console/v1/schema.graphql | 34 +- pkg/api/console/v1/schema/schema.go | 948 +++++++++++++++++- .../v1/types/control_state_transition.go | 59 ++ pkg/api/console/v1/types/types.go | 42 +- pkg/api/console/v1/v1_resolver.go | 12 + pkg/probo/coredata/control_state.go | 6 +- .../coredata/control_state_transition.go | 112 +++ pkg/probo/coredata/entity_type_reg.go | 1 + pkg/probo/probo.go | 28 + 9 files changed, 1211 insertions(+), 31 deletions(-) create mode 100644 pkg/api/console/v1/types/control_state_transition.go create mode 100644 pkg/probo/coredata/control_state_transition.go diff --git a/pkg/api/console/v1/schema.graphql b/pkg/api/console/v1/schema.graphql index 4e7b8d881..b28d54ec7 100644 --- a/pkg/api/console/v1/schema.graphql +++ b/pkg/api/console/v1/schema.graphql @@ -14,10 +14,10 @@ interface Node { } enum ControlState { - NotStarted - InProgress - NotApplicable - Implemented + NOT_STARTED + IN_PROGRESS + NOT_APPLICABLE + IMPLEMENTED } @@ -85,6 +85,13 @@ type Control implements Node { description: String! state: ControlState! + stateTransisions( + first: Int + after: CursorKey + last: Int + before: CursorKey + ): ControlStateTransitionConnection! @goField(forceResolver: true) + tasks( first: Int after: CursorKey @@ -96,6 +103,25 @@ type Control implements Node { updatedAt: Datetime! } +type ControlStateTransitionConnection { + edges: [ControlStateTransitionEdge!]! + pageInfo: PageInfo! +} + +type ControlStateTransitionEdge { + cursor: CursorKey! + node: ControlStateTransition! +} + +type ControlStateTransition { + id: ID! + fromState: ControlState + toState: ControlState! + reason: String + createdAt: Datetime! + updatedAt: Datetime! +} + type TaskConnection { edges: [TaskEdge!]! pageInfo: PageInfo! diff --git a/pkg/api/console/v1/schema/schema.go b/pkg/api/console/v1/schema/schema.go index 672445580..f43f6eef8 100644 --- a/pkg/api/console/v1/schema/schema.go +++ b/pkg/api/console/v1/schema/schema.go @@ -52,13 +52,14 @@ type DirectiveRoot struct { type ComplexityRoot struct { Control struct { - CreatedAt func(childComplexity int) int - 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 + CreatedAt func(childComplexity int) int + Description func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + State func(childComplexity int) int + StateTransisions func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int + Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int + UpdatedAt func(childComplexity int) int } ControlConnection struct { @@ -71,6 +72,25 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + ControlStateTransition struct { + CreatedAt func(childComplexity int) int + FromState func(childComplexity int) int + ID func(childComplexity int) int + Reason func(childComplexity int) int + ToState func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + + ControlStateTransitionConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + } + + ControlStateTransitionEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } + Framework struct { Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int CreatedAt func(childComplexity int) int @@ -128,6 +148,7 @@ type ComplexityRoot struct { } type ControlResolver interface { + StateTransisions(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlStateTransitionConnection, error) Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error) } type FrameworkResolver interface { @@ -194,6 +215,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Control.State(childComplexity), true + case "Control.stateTransisions": + if e.complexity.Control.StateTransisions == nil { + break + } + + args, err := ec.field_Control_stateTransisions_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Control.StateTransisions(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true + case "Control.tasks": if e.complexity.Control.Tasks == nil { break @@ -241,6 +274,76 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ControlEdge.Node(childComplexity), true + case "ControlStateTransition.createdAt": + if e.complexity.ControlStateTransition.CreatedAt == nil { + break + } + + return e.complexity.ControlStateTransition.CreatedAt(childComplexity), true + + case "ControlStateTransition.fromState": + if e.complexity.ControlStateTransition.FromState == nil { + break + } + + return e.complexity.ControlStateTransition.FromState(childComplexity), true + + case "ControlStateTransition.id": + if e.complexity.ControlStateTransition.ID == nil { + break + } + + return e.complexity.ControlStateTransition.ID(childComplexity), true + + case "ControlStateTransition.reason": + if e.complexity.ControlStateTransition.Reason == nil { + break + } + + return e.complexity.ControlStateTransition.Reason(childComplexity), true + + case "ControlStateTransition.toState": + if e.complexity.ControlStateTransition.ToState == nil { + break + } + + return e.complexity.ControlStateTransition.ToState(childComplexity), true + + case "ControlStateTransition.updatedAt": + if e.complexity.ControlStateTransition.UpdatedAt == nil { + break + } + + return e.complexity.ControlStateTransition.UpdatedAt(childComplexity), true + + case "ControlStateTransitionConnection.edges": + if e.complexity.ControlStateTransitionConnection.Edges == nil { + break + } + + return e.complexity.ControlStateTransitionConnection.Edges(childComplexity), true + + case "ControlStateTransitionConnection.pageInfo": + if e.complexity.ControlStateTransitionConnection.PageInfo == nil { + break + } + + return e.complexity.ControlStateTransitionConnection.PageInfo(childComplexity), true + + case "ControlStateTransitionEdge.cursor": + if e.complexity.ControlStateTransitionEdge.Cursor == nil { + break + } + + return e.complexity.ControlStateTransitionEdge.Cursor(childComplexity), true + + case "ControlStateTransitionEdge.node": + if e.complexity.ControlStateTransitionEdge.Node == nil { + break + } + + return e.complexity.ControlStateTransitionEdge.Node(childComplexity), true + case "Framework.controls": if e.complexity.Framework.Controls == nil { break @@ -557,10 +660,10 @@ interface Node { } enum ControlState { - NotStarted - InProgress - NotApplicable - Implemented + NOT_STARTED + IN_PROGRESS + NOT_APPLICABLE + IMPLEMENTED } @@ -628,6 +731,13 @@ type Control implements Node { description: String! state: ControlState! + stateTransisions( + first: Int + after: CursorKey + last: Int + before: CursorKey + ): ControlStateTransitionConnection! @goField(forceResolver: true) + tasks( first: Int after: CursorKey @@ -639,6 +749,25 @@ type Control implements Node { updatedAt: Datetime! } +type ControlStateTransitionConnection { + edges: [ControlStateTransitionEdge!]! + pageInfo: PageInfo! +} + +type ControlStateTransitionEdge { + cursor: CursorKey! + node: ControlStateTransition! +} + +type ControlStateTransition { + id: ID! + fromState: ControlState + toState: ControlState! + reason: String + createdAt: Datetime! + updatedAt: Datetime! +} + type TaskConnection { edges: [TaskEdge!]! pageInfo: PageInfo! @@ -667,6 +796,83 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Control_stateTransisions_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Control_stateTransisions_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Control_stateTransisions_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Control_stateTransisions_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Control_stateTransisions_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + return args, nil +} +func (ec *executionContext) field_Control_stateTransisions_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Control_stateTransisions_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Control_stateTransisions_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Control_stateTransisions_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + func (ec *executionContext) field_Control_tasks_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1150,6 +1356,55 @@ func (ec *executionContext) fieldContext_Control_state(_ context.Context, field return fc, nil } +func (ec *executionContext) _Control_stateTransisions(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Control_stateTransisions(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 ec.resolvers.Control().StateTransisions(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) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.ControlStateTransitionConnection) + fc.Result = res + return ec.marshalNControlStateTransitionConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Control_stateTransisions(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Control", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_ControlStateTransitionConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_ControlStateTransitionConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ControlStateTransitionConnection", field.Name) + }, + } + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Control_stateTransisions_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + 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 { @@ -1446,6 +1701,8 @@ func (ec *executionContext) fieldContext_ControlEdge_node(_ context.Context, fie return ec.fieldContext_Control_description(ctx, field) case "state": return ec.fieldContext_Control_state(ctx, field) + case "stateTransisions": + return ec.fieldContext_Control_stateTransisions(ctx, field) case "tasks": return ec.fieldContext_Control_tasks(ctx, field) case "createdAt": @@ -1459,6 +1716,410 @@ func (ec *executionContext) fieldContext_ControlEdge_node(_ context.Context, fie return fc, nil } +func (ec *executionContext) _ControlStateTransition_id(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransition_id(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.ID, 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.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransition_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransition_fromState(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransition_fromState(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.FromState, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*types.ControlState) + fc.Result = res + return ec.marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlState(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransition_fromState(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ControlState does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransition_toState(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransition_toState(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.ToState, 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.(types.ControlState) + fc.Result = res + return ec.marshalNControlState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlState(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransition_toState(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ControlState does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransition_reason(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransition_reason(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.Reason, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransition_reason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransition", + 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) _ControlStateTransition_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransition_createdAt(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.CreatedAt, 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.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransition_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransition_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransition_updatedAt(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.UpdatedAt, 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.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransition_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransitionConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransitionConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransitionConnection_edges(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.Edges, 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.([]*types.ControlStateTransitionEdge) + fc.Result = res + return ec.marshalNControlStateTransitionEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionEdgeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransitionConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransitionConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_ControlStateTransitionEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_ControlStateTransitionEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ControlStateTransitionEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransitionConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransitionConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransitionConnection_pageInfo(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.PageInfo, 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.(*types.PageInfo) + fc.Result = res + return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransitionConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransitionConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_PageInfo_hasNextPage(ctx, field) + case "hasPreviousPage": + return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field) + case "startCursor": + return ec.fieldContext_PageInfo_startCursor(ctx, field) + case "endCursor": + return ec.fieldContext_PageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransitionEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransitionEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransitionEdge_cursor(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.Cursor, 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.(page.CursorKey) + fc.Result = res + return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚋpageᚐCursorKey(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransitionEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransitionEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CursorKey does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ControlStateTransitionEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.ControlStateTransitionEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ControlStateTransitionEdge_node(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.Node, 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.(*types.ControlStateTransition) + fc.Result = res + return ec.marshalNControlStateTransition2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransition(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ControlStateTransitionEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ControlStateTransitionEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ControlStateTransition_id(ctx, field) + case "fromState": + return ec.fieldContext_ControlStateTransition_fromState(ctx, field) + case "toState": + return ec.fieldContext_ControlStateTransition_toState(ctx, field) + case "reason": + return ec.fieldContext_ControlStateTransition_reason(ctx, field) + case "createdAt": + return ec.fieldContext_ControlStateTransition_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_ControlStateTransition_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ControlStateTransition", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Framework_id(ctx context.Context, field graphql.CollectedField, obj *types.Framework) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Framework_id(ctx, field) if err != nil { @@ -4338,6 +4999,37 @@ func (ec *executionContext) _Control(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "stateTransisions": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + res = ec._Control_stateTransisions(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 "tasks": field := field @@ -4490,6 +5182,152 @@ func (ec *executionContext) _ControlEdge(ctx context.Context, sel ast.SelectionS return out } +var controlStateTransitionImplementors = []string{"ControlStateTransition"} + +func (ec *executionContext) _ControlStateTransition(ctx context.Context, sel ast.SelectionSet, obj *types.ControlStateTransition) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, controlStateTransitionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ControlStateTransition") + case "id": + out.Values[i] = ec._ControlStateTransition_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fromState": + out.Values[i] = ec._ControlStateTransition_fromState(ctx, field, obj) + case "toState": + out.Values[i] = ec._ControlStateTransition_toState(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "reason": + out.Values[i] = ec._ControlStateTransition_reason(ctx, field, obj) + case "createdAt": + out.Values[i] = ec._ControlStateTransition_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updatedAt": + out.Values[i] = ec._ControlStateTransition_updatedAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var controlStateTransitionConnectionImplementors = []string{"ControlStateTransitionConnection"} + +func (ec *executionContext) _ControlStateTransitionConnection(ctx context.Context, sel ast.SelectionSet, obj *types.ControlStateTransitionConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, controlStateTransitionConnectionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ControlStateTransitionConnection") + case "edges": + out.Values[i] = ec._ControlStateTransitionConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pageInfo": + out.Values[i] = ec._ControlStateTransitionConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var controlStateTransitionEdgeImplementors = []string{"ControlStateTransitionEdge"} + +func (ec *executionContext) _ControlStateTransitionEdge(ctx context.Context, sel ast.SelectionSet, obj *types.ControlStateTransitionEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, controlStateTransitionEdgeImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ControlStateTransitionEdge") + case "cursor": + out.Values[i] = ec._ControlStateTransitionEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._ControlStateTransitionEdge_node(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var frameworkImplementors = []string{"Framework", "Node"} func (ec *executionContext) _Framework(ctx context.Context, sel ast.SelectionSet, obj *types.Framework) graphql.Marshaler { @@ -5433,6 +6271,78 @@ func (ec *executionContext) marshalNControlState2githubᚗcomᚋgetproboᚋprobo return v } +func (ec *executionContext) marshalNControlStateTransition2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransition(ctx context.Context, sel ast.SelectionSet, v *types.ControlStateTransition) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ControlStateTransition(ctx, sel, v) +} + +func (ec *executionContext) marshalNControlStateTransitionConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionConnection(ctx context.Context, sel ast.SelectionSet, v types.ControlStateTransitionConnection) graphql.Marshaler { + return ec._ControlStateTransitionConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNControlStateTransitionConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionConnection(ctx context.Context, sel ast.SelectionSet, v *types.ControlStateTransitionConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ControlStateTransitionConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNControlStateTransitionEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.ControlStateTransitionEdge) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNControlStateTransitionEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionEdge(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNControlStateTransitionEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlStateTransitionEdge(ctx context.Context, sel ast.SelectionSet, v *types.ControlStateTransitionEdge) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ControlStateTransitionEdge(ctx, sel, 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) @@ -5912,6 +6822,22 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlState(ctx context.Context, v any) (*types.ControlState, error) { + if v == nil { + return nil, nil + } + var res = new(types.ControlState) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControlState(ctx context.Context, sel ast.SelectionSet, v *types.ControlState) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + 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 diff --git a/pkg/api/console/v1/types/control_state_transition.go b/pkg/api/console/v1/types/control_state_transition.go new file mode 100644 index 000000000..56904d8dc --- /dev/null +++ b/pkg/api/console/v1/types/control_state_transition.go @@ -0,0 +1,59 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "github.com/getprobo/probo/pkg/probo/coredata" + "github.com/getprobo/probo/pkg/probo/coredata/page" +) + +func NewControlStateTransitionConnection( + p *page.Page[*coredata.ControlStateTransition], +) *ControlStateTransitionConnection { + var edges = make([]*ControlStateTransitionEdge, len(p.Data)) + + for i := range edges { + edges[i] = NewControlStateTransitionEdge(p.Data[i]) + } + + return &ControlStateTransitionConnection{ + Edges: edges, + PageInfo: NewPageInfo(p), + } +} + +func NewControlStateTransitionEdge(cst *coredata.ControlStateTransition) *ControlStateTransitionEdge { + return &ControlStateTransitionEdge{ + Cursor: cst.CursorKey(), + Node: NewControlStateTransition(cst), + } +} + +func NewControlStateTransition(cst *coredata.ControlStateTransition) *ControlStateTransition { + var fromState *ControlState + if cst.FromState != nil { + val := ControlState((*cst.FromState).String()) + fromState = &val + } + + return &ControlStateTransition{ + ID: cst.ID, + FromState: fromState, + ToState: ControlState(cst.ToState.String()), + Reason: cst.Reason, + CreatedAt: cst.CreatedAt, + UpdatedAt: cst.UpdatedAt, + } +} diff --git a/pkg/api/console/v1/types/types.go b/pkg/api/console/v1/types/types.go index 34f925665..5acecce3b 100644 --- a/pkg/api/console/v1/types/types.go +++ b/pkg/api/console/v1/types/types.go @@ -18,13 +18,14 @@ type Node interface { } type Control struct { - ID gid.GID `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - State ControlState `json:"state"` - Tasks *TaskConnection `json:"tasks"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID gid.GID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + State ControlState `json:"state"` + StateTransisions *ControlStateTransitionConnection `json:"stateTransisions"` + Tasks *TaskConnection `json:"tasks"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } func (Control) IsNode() {} @@ -40,6 +41,25 @@ type ControlEdge struct { Node *Control `json:"node"` } +type ControlStateTransition struct { + ID gid.GID `json:"id"` + FromState *ControlState `json:"fromState,omitempty"` + ToState ControlState `json:"toState"` + Reason *string `json:"reason,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type ControlStateTransitionConnection struct { + Edges []*ControlStateTransitionEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` +} + +type ControlStateTransitionEdge struct { + Cursor page.CursorKey `json:"cursor"` + Node *ControlStateTransition `json:"node"` +} + type Framework struct { ID gid.GID `json:"id"` Name string `json:"name"` @@ -106,10 +126,10 @@ type TaskEdge struct { type ControlState string const ( - ControlStateNotStarted ControlState = "NotStarted" - ControlStateInProgress ControlState = "InProgress" - ControlStateNotApplicable ControlState = "NotApplicable" - ControlStateImplemented ControlState = "Implemented" + ControlStateNotStarted ControlState = "NOT_STARTED" + ControlStateInProgress ControlState = "IN_PROGRESS" + ControlStateNotApplicable ControlState = "NOT_APPLICABLE" + ControlStateImplemented ControlState = "IMPLEMENTED" ) var AllControlState = []ControlState{ diff --git a/pkg/api/console/v1/v1_resolver.go b/pkg/api/console/v1/v1_resolver.go index e95c0bbbf..4d8ebc2d4 100644 --- a/pkg/api/console/v1/v1_resolver.go +++ b/pkg/api/console/v1/v1_resolver.go @@ -16,6 +16,18 @@ import ( "github.com/vektah/gqlparser/v2/gqlerror" ) +// StateTransisions is the resolver for the stateTransisions field. +func (r *controlResolver) StateTransisions(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlStateTransitionConnection, error) { + cursor := types.NewCursor(first, after, last, before) + + page, err := r.svc.ListControlStateTransitions(ctx, obj.ID, cursor) + if err != nil { + return nil, fmt.Errorf("cannot list control tasks: %w", err) + } + + return types.NewControlStateTransitionConnection(page), nil +} + // Tasks is the resolver for the tasks field. func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error) { cursor := types.NewCursor(first, after, last, before) diff --git a/pkg/probo/coredata/control_state.go b/pkg/probo/coredata/control_state.go index c742ddce9..60dc7ea9e 100644 --- a/pkg/probo/coredata/control_state.go +++ b/pkg/probo/coredata/control_state.go @@ -17,8 +17,6 @@ package coredata import ( "database/sql/driver" "fmt" - - "go.gearno.de/x/panicf" ) type ( @@ -67,14 +65,12 @@ func (cs ControlState) String() string { val = "NOT_APPLICABLE" case ControlStateImplemented: val = "IMPLEMENTED" - default: - panicf.Panic("invalid control state value: %v", cs) } return val } -func (cs ControlState) Scan(value any) error { +func (cs *ControlState) Scan(value any) error { val, ok := value.(string) if !ok { return fmt.Errorf("invalid scan source for ControlState, expected string got %T", value) diff --git a/pkg/probo/coredata/control_state_transition.go b/pkg/probo/coredata/control_state_transition.go new file mode 100644 index 000000000..baed11159 --- /dev/null +++ b/pkg/probo/coredata/control_state_transition.go @@ -0,0 +1,112 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "fmt" + "maps" + "time" + + "github.com/getprobo/probo/pkg/probo/coredata/gid" + "github.com/getprobo/probo/pkg/probo/coredata/page" + "github.com/jackc/pgx/v5" + "go.gearno.de/crypto/uuid" + "go.gearno.de/kit/pg" +) + +type ( + ControlStateTransition struct { + ID gid.GID + ControlID gid.GID + FromState *ControlState + ToState ControlState + Reason *string + CreatedAt time.Time + UpdatedAt time.Time + } + + ControlStateTransitions []*ControlStateTransition +) + +func (cst ControlStateTransition) CursorKey() page.CursorKey { + return page.NewCursorKey(uuid.UUID(cst.ID), cst.CreatedAt) +} + +func (cst *ControlStateTransition) scan(r pgx.Row) error { + return r.Scan( + &cst.ID, + &cst.ControlID, + &cst.FromState, + &cst.ToState, + &cst.Reason, + &cst.CreatedAt, + &cst.UpdatedAt, + ) +} + +func (cst *ControlStateTransitions) LoadByControlID( + ctx context.Context, + conn pg.Conn, + scope *Scope, + controlID gid.GID, + cursor *page.Cursor, +) error { + q := ` +SELECT + id, + control_id, + from_state, + to_state, + reason, + created_at, + updated_at +FROM + control_state_transitions +WHERE + %s + AND control_id = @control_id + AND %s +` + + q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + + args := pgx.NamedArgs{"control_id": controlID} + maps.Copy(args, scope.SQLArguments()) + + r, err := conn.Query(ctx, q, args) + if err != nil { + return err + } + defer r.Close() + + controlStateTransitions := ControlStateTransitions{} + for r.Next() { + controlStateTransition := &ControlStateTransition{} + if err := controlStateTransition.scan(r); err != nil { + return err + } + + controlStateTransitions = append(controlStateTransitions, controlStateTransition) + } + + if err := r.Err(); err != nil { + return err + } + + *cst = controlStateTransitions + + return nil +} diff --git a/pkg/probo/coredata/entity_type_reg.go b/pkg/probo/coredata/entity_type_reg.go index 0b23a4c54..309bb944c 100644 --- a/pkg/probo/coredata/entity_type_reg.go +++ b/pkg/probo/coredata/entity_type_reg.go @@ -20,4 +20,5 @@ const ( ControlEntityType TaskEntityType EvidenceEntityType + ControlStateTransitionEntityType ) diff --git a/pkg/probo/probo.go b/pkg/probo/probo.go index 0399f78ec..4e61ce36c 100644 --- a/pkg/probo/probo.go +++ b/pkg/probo/probo.go @@ -149,3 +149,31 @@ func (s *Service) ListControlTasks( return page.NewPage(tasks, cursor), nil } + +func (s *Service) ListControlStateTransitions( + ctx context.Context, + controlID gid.GID, + cursor *page.Cursor, +) (*page.Page[*coredata.ControlStateTransition], error) { + var controlStateTransitions coredata.ControlStateTransitions + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return controlStateTransitions.LoadByControlID( + ctx, + conn, + s.scope, + controlID, + cursor, + ) + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(controlStateTransitions, cursor), nil +} +