diff --git a/pkg/api/console/v1/schema.graphql b/pkg/api/console/v1/schema.graphql index 719428aa4..42c9c1ea9 100644 --- a/pkg/api/console/v1/schema.graphql +++ b/pkg/api/console/v1/schema.graphql @@ -192,6 +192,13 @@ type Task implements Node { description: String! state: TaskState! + stateTransisions( + first: Int + after: CursorKey + last: Int + before: CursorKey + ): TaskStateTransitionConnection! @goField(forceResolver: true) + evidences( first: Int after: CursorKey @@ -203,6 +210,25 @@ type Task implements Node { updatedAt: Datetime! } +type TaskStateTransitionConnection { + edges: [TaskStateTransitionEdge!]! + pageInfo: PageInfo! +} + +type TaskStateTransitionEdge { + cursor: CursorKey! + node: TaskStateTransition! +} + +type TaskStateTransition { + id: ID! + fromState: TaskState + toState: TaskState! + reason: String + createdAt: Datetime! + updatedAt: Datetime! +} + type EvidenceConnection { edges: [EvidenceEdge!]! pageInfo: PageInfo! diff --git a/pkg/api/console/v1/schema/schema.go b/pkg/api/console/v1/schema/schema.go index 37282bef4..556564b76 100644 --- a/pkg/api/console/v1/schema/schema.go +++ b/pkg/api/console/v1/schema/schema.go @@ -169,13 +169,14 @@ type ComplexityRoot struct { } Task struct { - CreatedAt func(childComplexity int) int - Description func(childComplexity int) int - Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int - ID func(childComplexity int) int - Name func(childComplexity int) int - State func(childComplexity int) int - UpdatedAt func(childComplexity int) int + CreatedAt func(childComplexity int) int + Description func(childComplexity int) int + Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) 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 + UpdatedAt func(childComplexity int) int } TaskConnection struct { @@ -188,6 +189,25 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + TaskStateTransition 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 + } + + TaskStateTransitionConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + } + + TaskStateTransitionEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } + Vendor struct { CreatedAt func(childComplexity int) int ID func(childComplexity int) int @@ -222,6 +242,7 @@ type QueryResolver interface { Node(ctx context.Context, id gid.GID) (types.Node, error) } type TaskResolver interface { + StateTransisions(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskStateTransitionConnection, error) Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error) } @@ -760,6 +781,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Task.State(childComplexity), true + case "Task.stateTransisions": + if e.complexity.Task.StateTransisions == nil { + break + } + + args, err := ec.field_Task_stateTransisions_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Task.StateTransisions(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true + case "Task.updatedAt": if e.complexity.Task.UpdatedAt == nil { break @@ -795,6 +828,76 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.TaskEdge.Node(childComplexity), true + case "TaskStateTransition.createdAt": + if e.complexity.TaskStateTransition.CreatedAt == nil { + break + } + + return e.complexity.TaskStateTransition.CreatedAt(childComplexity), true + + case "TaskStateTransition.fromState": + if e.complexity.TaskStateTransition.FromState == nil { + break + } + + return e.complexity.TaskStateTransition.FromState(childComplexity), true + + case "TaskStateTransition.id": + if e.complexity.TaskStateTransition.ID == nil { + break + } + + return e.complexity.TaskStateTransition.ID(childComplexity), true + + case "TaskStateTransition.reason": + if e.complexity.TaskStateTransition.Reason == nil { + break + } + + return e.complexity.TaskStateTransition.Reason(childComplexity), true + + case "TaskStateTransition.toState": + if e.complexity.TaskStateTransition.ToState == nil { + break + } + + return e.complexity.TaskStateTransition.ToState(childComplexity), true + + case "TaskStateTransition.updatedAt": + if e.complexity.TaskStateTransition.UpdatedAt == nil { + break + } + + return e.complexity.TaskStateTransition.UpdatedAt(childComplexity), true + + case "TaskStateTransitionConnection.edges": + if e.complexity.TaskStateTransitionConnection.Edges == nil { + break + } + + return e.complexity.TaskStateTransitionConnection.Edges(childComplexity), true + + case "TaskStateTransitionConnection.pageInfo": + if e.complexity.TaskStateTransitionConnection.PageInfo == nil { + break + } + + return e.complexity.TaskStateTransitionConnection.PageInfo(childComplexity), true + + case "TaskStateTransitionEdge.cursor": + if e.complexity.TaskStateTransitionEdge.Cursor == nil { + break + } + + return e.complexity.TaskStateTransitionEdge.Cursor(childComplexity), true + + case "TaskStateTransitionEdge.node": + if e.complexity.TaskStateTransitionEdge.Node == nil { + break + } + + return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true + case "Vendor.createdAt": if e.complexity.Vendor.CreatedAt == nil { break @@ -1134,6 +1237,13 @@ type Task implements Node { description: String! state: TaskState! + stateTransisions( + first: Int + after: CursorKey + last: Int + before: CursorKey + ): TaskStateTransitionConnection! @goField(forceResolver: true) + evidences( first: Int after: CursorKey @@ -1145,6 +1255,25 @@ type Task implements Node { updatedAt: Datetime! } +type TaskStateTransitionConnection { + edges: [TaskStateTransitionEdge!]! + pageInfo: PageInfo! +} + +type TaskStateTransitionEdge { + cursor: CursorKey! + node: TaskStateTransition! +} + +type TaskStateTransition { + id: ID! + fromState: TaskState + toState: TaskState! + reason: String + createdAt: Datetime! + updatedAt: Datetime! +} + type EvidenceConnection { edges: [EvidenceEdge!]! pageInfo: PageInfo! @@ -1758,6 +1887,83 @@ func (ec *executionContext) field_Task_evidences_argsBefore( return zeroVal, nil } +func (ec *executionContext) field_Task_stateTransisions_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Task_stateTransisions_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Task_stateTransisions_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Task_stateTransisions_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Task_stateTransisions_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + return args, nil +} +func (ec *executionContext) field_Task_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_Task_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_Task_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_Task_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___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4640,6 +4846,55 @@ func (ec *executionContext) fieldContext_Task_state(_ context.Context, field gra return fc, nil } +func (ec *executionContext) _Task_stateTransisions(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Task_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.Task().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.TaskStateTransitionConnection) + fc.Result = res + return ec.marshalNTaskStateTransitionConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Task_stateTransisions(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Task", + 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_TaskStateTransitionConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_TaskStateTransitionConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TaskStateTransitionConnection", field.Name) + }, + } + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Task_stateTransisions_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Task_evidences(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Task_evidences(ctx, field) if err != nil { @@ -4936,6 +5191,8 @@ func (ec *executionContext) fieldContext_TaskEdge_node(_ context.Context, field return ec.fieldContext_Task_description(ctx, field) case "state": return ec.fieldContext_Task_state(ctx, field) + case "stateTransisions": + return ec.fieldContext_Task_stateTransisions(ctx, field) case "evidences": return ec.fieldContext_Task_evidences(ctx, field) case "createdAt": @@ -4949,6 +5206,410 @@ func (ec *executionContext) fieldContext_TaskEdge_node(_ context.Context, field return fc, nil } +func (ec *executionContext) _TaskStateTransition_id(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransition_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_TaskStateTransition_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransition", + 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) _TaskStateTransition_fromState(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransition_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.TaskState) + fc.Result = res + return ec.marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskState(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TaskStateTransition_fromState(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type TaskState does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _TaskStateTransition_toState(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransition_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.TaskState) + fc.Result = res + return ec.marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskState(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TaskStateTransition_toState(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type TaskState does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _TaskStateTransition_reason(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransition_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_TaskStateTransition_reason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransition", + 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) _TaskStateTransition_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransition_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_TaskStateTransition_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransition", + 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) _TaskStateTransition_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransition_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_TaskStateTransition_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransition", + 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) _TaskStateTransitionConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransitionConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransitionConnection_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.TaskStateTransitionEdge) + fc.Result = res + return ec.marshalNTaskStateTransitionEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionEdgeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TaskStateTransitionConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransitionConnection", + 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_TaskStateTransitionEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_TaskStateTransitionEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TaskStateTransitionEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _TaskStateTransitionConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransitionConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransitionConnection_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_TaskStateTransitionConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransitionConnection", + 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) _TaskStateTransitionEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransitionEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransitionEdge_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_TaskStateTransitionEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransitionEdge", + 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) _TaskStateTransitionEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.TaskStateTransitionEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TaskStateTransitionEdge_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.TaskStateTransition) + fc.Result = res + return ec.marshalNTaskStateTransition2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransition(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TaskStateTransitionEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TaskStateTransitionEdge", + 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_TaskStateTransition_id(ctx, field) + case "fromState": + return ec.fieldContext_TaskStateTransition_fromState(ctx, field) + case "toState": + return ec.fieldContext_TaskStateTransition_toState(ctx, field) + case "reason": + return ec.fieldContext_TaskStateTransition_reason(ctx, field) + case "createdAt": + return ec.fieldContext_TaskStateTransition_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_TaskStateTransition_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TaskStateTransition", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Vendor_id(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Vendor_id(ctx, field) if err != nil { @@ -8021,6 +8682,37 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj 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._Task_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 "evidences": field := field @@ -8173,6 +8865,152 @@ func (ec *executionContext) _TaskEdge(ctx context.Context, sel ast.SelectionSet, return out } +var taskStateTransitionImplementors = []string{"TaskStateTransition"} + +func (ec *executionContext) _TaskStateTransition(ctx context.Context, sel ast.SelectionSet, obj *types.TaskStateTransition) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, taskStateTransitionImplementors) + + 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("TaskStateTransition") + case "id": + out.Values[i] = ec._TaskStateTransition_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fromState": + out.Values[i] = ec._TaskStateTransition_fromState(ctx, field, obj) + case "toState": + out.Values[i] = ec._TaskStateTransition_toState(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "reason": + out.Values[i] = ec._TaskStateTransition_reason(ctx, field, obj) + case "createdAt": + out.Values[i] = ec._TaskStateTransition_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updatedAt": + out.Values[i] = ec._TaskStateTransition_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 taskStateTransitionConnectionImplementors = []string{"TaskStateTransitionConnection"} + +func (ec *executionContext) _TaskStateTransitionConnection(ctx context.Context, sel ast.SelectionSet, obj *types.TaskStateTransitionConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, taskStateTransitionConnectionImplementors) + + 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("TaskStateTransitionConnection") + case "edges": + out.Values[i] = ec._TaskStateTransitionConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pageInfo": + out.Values[i] = ec._TaskStateTransitionConnection_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 taskStateTransitionEdgeImplementors = []string{"TaskStateTransitionEdge"} + +func (ec *executionContext) _TaskStateTransitionEdge(ctx context.Context, sel ast.SelectionSet, obj *types.TaskStateTransitionEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, taskStateTransitionEdgeImplementors) + + 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("TaskStateTransitionEdge") + case "cursor": + out.Values[i] = ec._TaskStateTransitionEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._TaskStateTransitionEdge_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 vendorImplementors = []string{"Vendor", "Node"} func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, obj *types.Vendor) graphql.Marshaler { @@ -9220,6 +10058,78 @@ func (ec *executionContext) marshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋ return v } +func (ec *executionContext) marshalNTaskStateTransition2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransition(ctx context.Context, sel ast.SelectionSet, v *types.TaskStateTransition) 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._TaskStateTransition(ctx, sel, v) +} + +func (ec *executionContext) marshalNTaskStateTransitionConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionConnection(ctx context.Context, sel ast.SelectionSet, v types.TaskStateTransitionConnection) graphql.Marshaler { + return ec._TaskStateTransitionConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNTaskStateTransitionConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionConnection(ctx context.Context, sel ast.SelectionSet, v *types.TaskStateTransitionConnection) 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._TaskStateTransitionConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNTaskStateTransitionEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.TaskStateTransitionEdge) 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.marshalNTaskStateTransitionEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionEdge(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) marshalNTaskStateTransitionEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskStateTransitionEdge(ctx context.Context, sel ast.SelectionSet, v *types.TaskStateTransitionEdge) 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._TaskStateTransitionEdge(ctx, sel, v) +} + func (ec *executionContext) marshalNVendor2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v *types.Vendor) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -9611,6 +10521,22 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } +func (ec *executionContext) unmarshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskState(ctx context.Context, v any) (*types.TaskState, error) { + if v == nil { + return nil, nil + } + var res = new(types.TaskState) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOTaskState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskState(ctx context.Context, sel ast.SelectionSet, v *types.TaskState) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/api/console/v1/types/task_state_transition.go b/pkg/api/console/v1/types/task_state_transition.go new file mode 100644 index 000000000..742757069 --- /dev/null +++ b/pkg/api/console/v1/types/task_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 NewTaskStateTransitionConnection( + p *page.Page[*coredata.TaskStateTransition], +) *TaskStateTransitionConnection { + var edges = make([]*TaskStateTransitionEdge, len(p.Data)) + + for i := range edges { + edges[i] = NewTaskStateTransitionEdge(p.Data[i]) + } + + return &TaskStateTransitionConnection{ + Edges: edges, + PageInfo: NewPageInfo(p), + } +} + +func NewTaskStateTransitionEdge(tst *coredata.TaskStateTransition) *TaskStateTransitionEdge { + return &TaskStateTransitionEdge{ + Cursor: tst.CursorKey(), + Node: NewTaskStateTransition(tst), + } +} + +func NewTaskStateTransition(tst *coredata.TaskStateTransition) *TaskStateTransition { + var fromState *TaskState + if tst.FromState != nil { + val := TaskState((*tst.FromState).String()) + fromState = &val + } + + return &TaskStateTransition{ + ID: tst.ID, + FromState: fromState, + ToState: TaskState(tst.ToState.String()), + Reason: tst.Reason, + CreatedAt: tst.CreatedAt, + UpdatedAt: tst.UpdatedAt, + } +} diff --git a/pkg/api/console/v1/types/types.go b/pkg/api/console/v1/types/types.go index 7800c9046..0773bc1de 100644 --- a/pkg/api/console/v1/types/types.go +++ b/pkg/api/console/v1/types/types.go @@ -148,13 +148,14 @@ type Query struct { } type Task struct { - ID gid.GID `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - State TaskState `json:"state"` - Evidences *EvidenceConnection `json:"evidences"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID gid.GID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + State TaskState `json:"state"` + StateTransisions *TaskStateTransitionConnection `json:"stateTransisions"` + Evidences *EvidenceConnection `json:"evidences"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } func (Task) IsNode() {} @@ -170,6 +171,25 @@ type TaskEdge struct { Node *Task `json:"node"` } +type TaskStateTransition struct { + ID gid.GID `json:"id"` + FromState *TaskState `json:"fromState,omitempty"` + ToState TaskState `json:"toState"` + Reason *string `json:"reason,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type TaskStateTransitionConnection struct { + Edges []*TaskStateTransitionEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` +} + +type TaskStateTransitionEdge struct { + Cursor page.CursorKey `json:"cursor"` + Node *TaskStateTransition `json:"node"` +} + type Vendor struct { ID gid.GID `json:"id"` Name string `json:"name"` diff --git a/pkg/api/console/v1/v1_resolver.go b/pkg/api/console/v1/v1_resolver.go index f7707598b..96a4ce1e3 100644 --- a/pkg/api/console/v1/v1_resolver.go +++ b/pkg/api/console/v1/v1_resolver.go @@ -104,6 +104,18 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return nil, gqlerror.Errorf("node %q not found", id) } +// StateTransisions is the resolver for the stateTransisions field. +func (r *taskResolver) StateTransisions(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskStateTransitionConnection, error) { + cursor := types.NewCursor(first, after, last, before) + + page, err := r.svc.ListTaskStateTransitions(ctx, obj.ID, cursor) + if err != nil { + return nil, fmt.Errorf("cannot list control tasks: %w", err) + } + + return types.NewTaskStateTransitionConnection(page), nil +} + // Evidences is the resolver for the evidences field. func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error) { cursor := types.NewCursor(first, after, last, before) diff --git a/pkg/probo/coredata/control_state_transition.go b/pkg/probo/coredata/control_state_transition.go index baed11159..884c539ae 100644 --- a/pkg/probo/coredata/control_state_transition.go +++ b/pkg/probo/coredata/control_state_transition.go @@ -18,7 +18,6 @@ import ( "context" "fmt" "maps" - "time" "github.com/getprobo/probo/pkg/probo/coredata/gid" "github.com/getprobo/probo/pkg/probo/coredata/page" @@ -29,13 +28,9 @@ import ( type ( ControlStateTransition struct { - ID gid.GID + StateTransition[ControlState] + ControlID gid.GID - FromState *ControlState - ToState ControlState - Reason *string - CreatedAt time.Time - UpdatedAt time.Time } ControlStateTransitions []*ControlStateTransition diff --git a/pkg/probo/coredata/entity_type_reg.go b/pkg/probo/coredata/entity_type_reg.go index 309bb944c..41d8e505c 100644 --- a/pkg/probo/coredata/entity_type_reg.go +++ b/pkg/probo/coredata/entity_type_reg.go @@ -21,4 +21,7 @@ const ( TaskEntityType EvidenceEntityType ControlStateTransitionEntityType + TaskStateTransitionEntityType + VendorEntityType + PeopleEntityType ) diff --git a/pkg/probo/coredata/migrations/20250130T154300Z.sql b/pkg/probo/coredata/migrations/20250130T154300Z.sql new file mode 100644 index 000000000..642f2cc0d --- /dev/null +++ b/pkg/probo/coredata/migrations/20250130T154300Z.sql @@ -0,0 +1,11 @@ +CREATE TABLE task_state_transitions ( + id TEXT PRIMARY KEY, + task_id TEXT REFERENCES tasks(id) NOT NULL, + from_state task_state, + to_state task_state NOT NULL, + reason TEXT, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE +); + +ALTER TABLE tasks DROP COLUMN state; diff --git a/pkg/probo/coredata/state_transition.go b/pkg/probo/coredata/state_transition.go new file mode 100644 index 000000000..823d9c1c5 --- /dev/null +++ b/pkg/probo/coredata/state_transition.go @@ -0,0 +1,31 @@ +// +// 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 ( + "time" + + "github.com/getprobo/probo/pkg/probo/coredata/gid" +) + +type ( + StateTransition[T any] struct { + ID gid.GID + FromState *T + ToState T + Reason *string + CreatedAt time.Time + UpdatedAt time.Time + } +) diff --git a/pkg/probo/coredata/task.go b/pkg/probo/coredata/task.go index 11b2c691c..42efbf0ce 100644 --- a/pkg/probo/coredata/task.go +++ b/pkg/probo/coredata/task.go @@ -68,36 +68,49 @@ func (t *Tasks) LoadByControlID( cursor *page.Cursor, ) error { q := ` -WITH control_tasks AS ( - SELECT - t.id, - @control_id AS control_id, - t.name, - t.description, - t.state, - t.content_ref, - t.created_at, - t.updated_at - FROM - tasks t - INNER JOIN - controls_tasks ct ON - ct.task_id = t.id - AND ct.control_id = @control_id - WHERE - %s -) +WITH + control_tasks AS ( + SELECT + t.id, + @control_id AS control_id, + t.name, + t.description, + t.content_ref, + t.created_at, + t.updated_at + FROM + tasks t + INNER JOIN + controls_tasks ct ON + ct.task_id = t.id + AND ct.control_id = @control_id + WHERE + %s + ), + task_states AS ( + SELECT + task_id, + to_state AS state, + reason, + RANK() OVER w + FROM + task_state_transitions + WINDOW + w AS (PARTITION BY task_id ORDER BY created_at DESC) + ) SELECT id, control_id, name, description, - state, + ts.state AS state, content_ref, created_at, updated_at FROM control_tasks +INNER JOIN + task_states ts ON ts.task_id = control_tasks.id WHERE %s ` diff --git a/pkg/probo/coredata/task_state_transition.go b/pkg/probo/coredata/task_state_transition.go new file mode 100644 index 000000000..12791fee1 --- /dev/null +++ b/pkg/probo/coredata/task_state_transition.go @@ -0,0 +1,107 @@ +// 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" + + "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 ( + TaskStateTransition struct { + TaskID gid.GID + + StateTransition[TaskState] + } + + TaskStateTransitions []*TaskStateTransition +) + +func (tst TaskStateTransition) CursorKey() page.CursorKey { + return page.NewCursorKey(uuid.UUID(tst.ID), tst.CreatedAt) +} + +func (tst *TaskStateTransition) scan(r pgx.Row) error { + return r.Scan( + &tst.ID, + &tst.TaskID, + &tst.FromState, + &tst.ToState, + &tst.Reason, + &tst.CreatedAt, + &tst.UpdatedAt, + ) +} + +func (tst *TaskStateTransitions) LoadByTaskID( + ctx context.Context, + conn pg.Conn, + scope *Scope, + taskID gid.GID, + cursor *page.Cursor, +) error { + q := ` +SELECT + id, + task_id, + from_state, + to_state, + reason, + created_at, + updated_at +FROM + task_state_transitions +WHERE + %s + AND task_id = @task_id + AND %s +` + + q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + + args := pgx.NamedArgs{"task_id": taskID} + maps.Copy(args, scope.SQLArguments()) + + r, err := conn.Query(ctx, q, args) + if err != nil { + return err + } + defer r.Close() + + taskStateTransitions := TaskStateTransitions{} + for r.Next() { + taskStateTransition := &TaskStateTransition{} + if err := taskStateTransition.scan(r); err != nil { + return err + } + + taskStateTransitions = append(taskStateTransitions, taskStateTransition) + } + + if err := r.Err(); err != nil { + return err + } + + *tst = taskStateTransitions + + return nil +} diff --git a/pkg/probo/probo.go b/pkg/probo/probo.go index af67c00ac..1d1f02fa3 100644 --- a/pkg/probo/probo.go +++ b/pkg/probo/probo.go @@ -257,3 +257,30 @@ func (s Service) ListOrganizationPeoples( return page.NewPage(peoples, cursor), nil } + +func (s Service) ListTaskStateTransitions( + ctx context.Context, + taskID gid.GID, + cursor *page.Cursor, +) (*page.Page[*coredata.TaskStateTransition], error) { + var taskStateTransitions coredata.TaskStateTransitions + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return taskStateTransitions.LoadByTaskID( + ctx, + conn, + s.scope, + taskID, + cursor, + ) + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(taskStateTransitions, cursor), nil +}