Add evidence state

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-01-30 21:28:12 -08:00
parent 925e91b96e
commit 9c0c5afa9c
10 changed files with 351 additions and 8 deletions

View File

@@ -25,6 +25,12 @@ enum TaskState {
DONE DONE
} }
enum EvidenceState {
VALID
INVALID
EXPIRED
}
type PageInfo { type PageInfo {
hasNextPage: Boolean! hasNextPage: Boolean!
hasPreviousPage: Boolean! hasPreviousPage: Boolean!
@@ -242,6 +248,7 @@ type EvidenceEdge {
type Evidence implements Node { type Evidence implements Node {
id: ID! id: ID!
fileUrl: String! fileUrl: String!
state: EvidenceState!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
} }

View File

@@ -96,6 +96,7 @@ type ComplexityRoot struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
FileURL func(childComplexity int) int FileURL func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
State func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
} }
@@ -450,6 +451,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Evidence.ID(childComplexity), true return e.complexity.Evidence.ID(childComplexity), true
case "Evidence.state":
if e.complexity.Evidence.State == nil {
break
}
return e.complexity.Evidence.State(childComplexity), true
case "Evidence.updatedAt": case "Evidence.updatedAt":
if e.complexity.Evidence.UpdatedAt == nil { if e.complexity.Evidence.UpdatedAt == nil {
break break
@@ -1070,6 +1078,12 @@ enum TaskState {
DONE DONE
} }
enum EvidenceState {
VALID
INVALID
EXPIRED
}
type PageInfo { type PageInfo {
hasNextPage: Boolean! hasNextPage: Boolean!
hasPreviousPage: Boolean! hasPreviousPage: Boolean!
@@ -1287,6 +1301,7 @@ type EvidenceEdge {
type Evidence implements Node { type Evidence implements Node {
id: ID! id: ID!
fileUrl: String! fileUrl: String!
state: EvidenceState!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
} }
@@ -3010,6 +3025,44 @@ func (ec *executionContext) fieldContext_Evidence_fileUrl(_ context.Context, fie
return fc, nil return fc, nil
} }
func (ec *executionContext) _Evidence_state(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_state(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.State, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(types.EvidenceState)
fc.Result = res
return ec.marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐEvidenceState(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Evidence_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Evidence",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type EvidenceState does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Evidence_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) { func (ec *executionContext) _Evidence_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_createdAt(ctx, field) fc, err := ec.fieldContext_Evidence_createdAt(ctx, field)
if err != nil { if err != nil {
@@ -3253,6 +3306,8 @@ func (ec *executionContext) fieldContext_EvidenceEdge_node(_ context.Context, fi
return ec.fieldContext_Evidence_id(ctx, field) return ec.fieldContext_Evidence_id(ctx, field)
case "fileUrl": case "fileUrl":
return ec.fieldContext_Evidence_fileUrl(ctx, field) return ec.fieldContext_Evidence_fileUrl(ctx, field)
case "state":
return ec.fieldContext_Evidence_state(ctx, field)
case "createdAt": case "createdAt":
return ec.fieldContext_Evidence_createdAt(ctx, field) return ec.fieldContext_Evidence_createdAt(ctx, field)
case "updatedAt": case "updatedAt":
@@ -7938,6 +7993,11 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "state":
out.Values[i] = ec._Evidence_state(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createdAt": case "createdAt":
out.Values[i] = ec._Evidence_createdAt(ctx, field, obj) out.Values[i] = ec._Evidence_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -9750,6 +9810,16 @@ func (ec *executionContext) marshalNEvidenceEdge2ᚖgithubᚗcomᚋgetproboᚋpr
return ec._EvidenceEdge(ctx, sel, v) return ec._EvidenceEdge(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐEvidenceState(ctx context.Context, v any) (types.EvidenceState, error) {
var res types.EvidenceState
err := res.UnmarshalGQL(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNEvidenceState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐEvidenceState(ctx context.Context, sel ast.SelectionSet, v types.EvidenceState) graphql.Marshaler {
return v
}
func (ec *executionContext) marshalNFramework2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v *types.Framework) graphql.Marshaler { func (ec *executionContext) marshalNFramework2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v *types.Framework) graphql.Marshaler {
if v == nil { if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@@ -39,11 +39,12 @@ func NewEvidenceEdge(e *coredata.Evidence) *EvidenceEdge {
} }
} }
func NewEvidence(c *coredata.Evidence) *Evidence { func NewEvidence(e *coredata.Evidence) *Evidence {
return &Evidence{ return &Evidence{
ID: c.ID, ID: e.ID,
State: EvidenceState(e.State.String()),
FileURL: "", FileURL: "",
CreatedAt: c.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: c.UpdatedAt, UpdatedAt: e.UpdatedAt,
} }
} }

View File

@@ -63,6 +63,7 @@ type ControlStateTransitionEdge struct {
type Evidence struct { type Evidence struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
FileURL string `json:"fileUrl"` FileURL string `json:"fileUrl"`
State EvidenceState `json:"state"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
@@ -255,6 +256,49 @@ func (e ControlState) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
type EvidenceState string
const (
EvidenceStateValid EvidenceState = "VALID"
EvidenceStateInvalid EvidenceState = "INVALID"
EvidenceStateExpired EvidenceState = "EXPIRED"
)
var AllEvidenceState = []EvidenceState{
EvidenceStateValid,
EvidenceStateInvalid,
EvidenceStateExpired,
}
func (e EvidenceState) IsValid() bool {
switch e {
case EvidenceStateValid, EvidenceStateInvalid, EvidenceStateExpired:
return true
}
return false
}
func (e EvidenceState) String() string {
return string(e)
}
func (e *EvidenceState) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = EvidenceState(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid EvidenceState", str)
}
return nil
}
func (e EvidenceState) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type TaskState string type TaskState string
const ( const (

View File

@@ -24,4 +24,5 @@ const (
TaskStateTransitionEntityType TaskStateTransitionEntityType
VendorEntityType VendorEntityType
PeopleEntityType PeopleEntityType
EvidenceStateTransitionEntityType
) )

View File

@@ -31,6 +31,7 @@ type (
Evidence struct { Evidence struct {
ID gid.GID ID gid.GID
TaskID gid.GID TaskID gid.GID
State EvidenceState
ObjectKey string ObjectKey string
MimeType string MimeType string
Size uint64 Size uint64
@@ -49,6 +50,7 @@ func (e *Evidence) scan(r pgx.Row) error {
return r.Scan( return r.Scan(
&e.ID, &e.ID,
&e.TaskID, &e.TaskID,
&e.State,
&e.ObjectKey, &e.ObjectKey,
&e.MimeType, &e.MimeType,
&e.Size, &e.Size,
@@ -65,9 +67,22 @@ func (e *Evidences) LoadByTaskID(
cursor *page.Cursor, cursor *page.Cursor,
) error { ) error {
q := ` q := `
WITH
evidence_states AS (
SELECT
evidence_id,
to_state AS state,
reason,
RANK() OVER w
FROM
evidence_state_transitions
WINDOW
w AS (PARTITION BY evidence_id ORDER BY created_at DESC)
)
SELECT SELECT
id, id,
task_id, task_id,
es.state,
object_key, object_key,
mime_type, mime_type,
size, size,
@@ -75,9 +90,12 @@ SELECT
updated_at updated_at
FROM FROM
evidences evidences
INNER JOIN
evidence_states es ON es.evidence_id = evidences.id
WHERE WHERE
%s %s
AND task_id = @task_id AND task_id = @task_id
AND es.rank = 1
AND %s AND %s
` `

View File

@@ -0,0 +1,79 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// 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 (
"database/sql/driver"
"fmt"
)
type (
EvidenceState uint8
)
const (
EvidenceStateValid EvidenceState = iota
EvidenceStateInvalid
EvidenceStateNotExpired
)
func (es EvidenceState) MarshalText() ([]byte, error) {
return []byte(es.String()), nil
}
func (es *EvidenceState) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case EvidenceStateValid.String():
*es = EvidenceStateValid
case EvidenceStateInvalid.String():
*es = EvidenceStateInvalid
case EvidenceStateNotExpired.String():
*es = EvidenceStateNotExpired
default:
return fmt.Errorf("invalid EvidenceState value: %q", val)
}
return nil
}
func (es EvidenceState) String() string {
var val string
switch es {
case EvidenceStateValid:
val = "VALID"
case EvidenceStateInvalid:
val = "INVALID"
case EvidenceStateNotExpired:
val = "EXPIRED"
}
return val
}
func (es *EvidenceState) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for EvidenceState, expected string got %T", value)
}
return es.UnmarshalText([]byte(val))
}
func (es EvidenceState) Value() (driver.Value, error) {
return es.String(), nil
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// 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 (
EvidenceStateTransition struct {
StateTransition[EvidenceState]
EvidenceID gid.GID
}
EvidenceStateTransitions []*EvidenceStateTransition
)
func (cst EvidenceStateTransition) CursorKey() page.CursorKey {
return page.NewCursorKey(uuid.UUID(cst.ID), cst.CreatedAt)
}
func (cst *EvidenceStateTransition) scan(r pgx.Row) error {
return r.Scan(
&cst.ID,
&cst.EvidenceID,
&cst.FromState,
&cst.ToState,
&cst.Reason,
&cst.CreatedAt,
&cst.UpdatedAt,
)
}
func (cst *EvidenceStateTransitions) LoadByEvidenceID(
ctx context.Context,
conn pg.Conn,
scope *Scope,
evidenceID gid.GID,
cursor *page.Cursor,
) error {
q := `
SELECT
id,
evidence_id,
from_state,
to_state,
reason,
created_at,
updated_at
FROM
evidence_state_transitions
WHERE
%s
AND evidence_id = @evidence_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"evidence_id": evidenceID}
maps.Copy(args, scope.SQLArguments())
r, err := conn.Query(ctx, q, args)
if err != nil {
return err
}
defer r.Close()
evidenceStateTransitions := EvidenceStateTransitions{}
for r.Next() {
evidenceStateTransition := &EvidenceStateTransition{}
if err := evidenceStateTransition.scan(r); err != nil {
return err
}
evidenceStateTransitions = append(evidenceStateTransitions, evidenceStateTransition)
}
if err := r.Err(); err != nil {
return err
}
*cst = evidenceStateTransitions
return nil
}

View File

@@ -0,0 +1,14 @@
CREATE TYPE evidence_state AS ENUM (
'VALID',
'INVALID',
'EXPIRED'
);
CREATE TABLE evidence_state_transitions (
id TEXT PRIMARY KEY,
from_state evidence_state,
to_state evidence_state NOT NULL,
reason TEXT,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE evidence_state_transitions ADD COLUMN evidence_id TEXT REFERENCES evidences(id) NOT NULL;
ALTER TABLE evidences ALTER COLUMN size TYPE BIGINT USING size::BIGINT;