Add finding coredata types and queries
Introduce the Finding, FindingAudit, FindingKind, FindingStatus, FindingPriority, FindingFilter, and FindingOrderField types in the coredata layer. Add CRUD operations, list with filtering/pagination, and audit association queries. Remove the now-replaced nonconformity and continual_improvement coredata types. Update entity type registry and snapshot types to reference the new findings type. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -452,6 +452,157 @@ WHERE %s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Audits) LoadByFindingID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
findingID gid.GID,
|
||||||
|
cursor *page.Cursor[AuditOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH audits_by_finding AS (
|
||||||
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.tenant_id,
|
||||||
|
a.name,
|
||||||
|
a.organization_id,
|
||||||
|
a.framework_id,
|
||||||
|
a.report_id,
|
||||||
|
a.valid_from,
|
||||||
|
a.valid_until,
|
||||||
|
a.state,
|
||||||
|
a.trust_center_visibility,
|
||||||
|
a.created_at,
|
||||||
|
a.updated_at
|
||||||
|
FROM
|
||||||
|
audits a
|
||||||
|
INNER JOIN
|
||||||
|
findings_audits fa ON a.id = fa.audit_id
|
||||||
|
WHERE
|
||||||
|
fa.finding_id = @finding_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
organization_id,
|
||||||
|
framework_id,
|
||||||
|
report_id,
|
||||||
|
valid_from,
|
||||||
|
valid_until,
|
||||||
|
state,
|
||||||
|
trust_center_visibility,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
audits_by_finding
|
||||||
|
WHERE %s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"finding_id": findingID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query audits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
audits, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Audit])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect audits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = audits
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Audits) CountByControlID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
controlID gid.GID,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
WITH audits_by_control AS (
|
||||||
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.tenant_id
|
||||||
|
FROM
|
||||||
|
audits a
|
||||||
|
INNER JOIN
|
||||||
|
controls_audits ca ON a.id = ca.audit_id
|
||||||
|
WHERE
|
||||||
|
ca.control_id = @control_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(id)
|
||||||
|
FROM
|
||||||
|
audits_by_control
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count audits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Audits) CountByFindingID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
findingID gid.GID,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
WITH audits_by_finding AS (
|
||||||
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.tenant_id
|
||||||
|
FROM
|
||||||
|
audits a
|
||||||
|
INNER JOIN
|
||||||
|
findings_audits fa ON a.id = fa.audit_id
|
||||||
|
WHERE
|
||||||
|
fa.finding_id = @finding_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(id)
|
||||||
|
FROM
|
||||||
|
audits_by_finding
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"finding_id": findingID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count audits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Audit) LoadByReportID(
|
func (a *Audit) LoadByReportID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
ContinualImprovementFilter struct {
|
|
||||||
snapshotID **gid.GID
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewContinualImprovementFilter(snapshotID **gid.GID) *ContinualImprovementFilter {
|
|
||||||
return &ContinualImprovementFilter{
|
|
||||||
snapshotID: snapshotID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *ContinualImprovementFilter) SQLArguments() pgx.NamedArgs {
|
|
||||||
args := pgx.NamedArgs{}
|
|
||||||
|
|
||||||
if f.snapshotID != nil && *f.snapshotID != nil {
|
|
||||||
args["filter_snapshot_id"] = **f.snapshotID
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *ContinualImprovementFilter) SQLFragment() string {
|
|
||||||
if f.snapshotID == nil {
|
|
||||||
return "TRUE"
|
|
||||||
}
|
|
||||||
|
|
||||||
if *f.snapshotID == nil {
|
|
||||||
return "snapshot_id IS NULL"
|
|
||||||
} else {
|
|
||||||
return "snapshot_id = @filter_snapshot_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContinualImprovementOrderField string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContinualImprovementOrderFieldCreatedAt ContinualImprovementOrderField = "CREATED_AT"
|
|
||||||
ContinualImprovementOrderFieldTargetDate ContinualImprovementOrderField = "TARGET_DATE"
|
|
||||||
ContinualImprovementOrderFieldStatus ContinualImprovementOrderField = "STATUS"
|
|
||||||
ContinualImprovementOrderFieldPriority ContinualImprovementOrderField = "PRIORITY"
|
|
||||||
ContinualImprovementOrderFieldReferenceId ContinualImprovementOrderField = "REFERENCE_ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p ContinualImprovementOrderField) Column() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ContinualImprovementOrderField) String() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ContinualImprovementOrderField) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(p.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ContinualImprovementOrderField) UnmarshalText(text []byte) error {
|
|
||||||
val := string(text)
|
|
||||||
switch val {
|
|
||||||
case string(ContinualImprovementOrderFieldCreatedAt),
|
|
||||||
string(ContinualImprovementOrderFieldTargetDate),
|
|
||||||
string(ContinualImprovementOrderFieldStatus),
|
|
||||||
string(ContinualImprovementOrderFieldPriority),
|
|
||||||
string(ContinualImprovementOrderFieldReferenceId):
|
|
||||||
*p = ContinualImprovementOrderField(val)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("invalid ContinualImprovementOrderField value: %q", val)
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// 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 ContinualImprovementPriority string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContinualImprovementPriorityLow ContinualImprovementPriority = "LOW"
|
|
||||||
ContinualImprovementPriorityMedium ContinualImprovementPriority = "MEDIUM"
|
|
||||||
ContinualImprovementPriorityHigh ContinualImprovementPriority = "HIGH"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ContinualImprovementPriorities() []ContinualImprovementPriority {
|
|
||||||
return []ContinualImprovementPriority{
|
|
||||||
ContinualImprovementPriorityLow,
|
|
||||||
ContinualImprovementPriorityMedium,
|
|
||||||
ContinualImprovementPriorityHigh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cip ContinualImprovementPriority) String() string {
|
|
||||||
return string(cip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cip *ContinualImprovementPriority) Scan(value any) error {
|
|
||||||
var s string
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
s = v
|
|
||||||
case []byte:
|
|
||||||
s = string(v)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type for ContinualImprovementPriority: %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s {
|
|
||||||
case "LOW":
|
|
||||||
*cip = ContinualImprovementPriorityLow
|
|
||||||
case "MEDIUM":
|
|
||||||
*cip = ContinualImprovementPriorityMedium
|
|
||||||
case "HIGH":
|
|
||||||
*cip = ContinualImprovementPriorityHigh
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid ContinualImprovementPriority value: %q", s)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cip ContinualImprovementPriority) Value() (driver.Value, error) {
|
|
||||||
return cip.String(), nil
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// 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 ContinualImprovementStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContinualImprovementStatusOpen ContinualImprovementStatus = "OPEN"
|
|
||||||
ContinualImprovementStatusInProgress ContinualImprovementStatus = "IN_PROGRESS"
|
|
||||||
ContinualImprovementStatusClosed ContinualImprovementStatus = "CLOSED"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ContinualImprovementStatuses() []ContinualImprovementStatus {
|
|
||||||
return []ContinualImprovementStatus{
|
|
||||||
ContinualImprovementStatusOpen,
|
|
||||||
ContinualImprovementStatusInProgress,
|
|
||||||
ContinualImprovementStatusClosed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cis ContinualImprovementStatus) String() string {
|
|
||||||
return string(cis)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cis *ContinualImprovementStatus) Scan(value any) error {
|
|
||||||
var s string
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
s = v
|
|
||||||
case []byte:
|
|
||||||
s = string(v)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type for ContinualImprovementStatus: %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s {
|
|
||||||
case "OPEN":
|
|
||||||
*cis = ContinualImprovementStatusOpen
|
|
||||||
case "IN_PROGRESS":
|
|
||||||
*cis = ContinualImprovementStatusInProgress
|
|
||||||
case "CLOSED":
|
|
||||||
*cis = ContinualImprovementStatusClosed
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid ContinualImprovementStatus value: %q", s)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cis ContinualImprovementStatus) Value() (driver.Value, error) {
|
|
||||||
return cis.String(), nil
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
ContinualImprovement struct {
|
|
||||||
ID gid.GID `db:"id"`
|
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
|
||||||
ReferenceID string `db:"reference_id"`
|
|
||||||
Description *string `db:"description"`
|
|
||||||
Source *string `db:"source"`
|
|
||||||
OwnerID gid.GID `db:"owner_profile_id"`
|
|
||||||
TargetDate *time.Time `db:"target_date"`
|
|
||||||
Status ContinualImprovementStatus `db:"status"`
|
|
||||||
Priority ContinualImprovementPriority `db:"priority"`
|
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
ContinualImprovements []*ContinualImprovement
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ci *ContinualImprovement) CursorKey(field ContinualImprovementOrderField) page.CursorKey {
|
|
||||||
switch field {
|
|
||||||
case ContinualImprovementOrderFieldCreatedAt:
|
|
||||||
return page.NewCursorKey(ci.ID, ci.CreatedAt)
|
|
||||||
case ContinualImprovementOrderFieldTargetDate:
|
|
||||||
return page.NewCursorKey(ci.ID, ci.TargetDate)
|
|
||||||
case ContinualImprovementOrderFieldStatus:
|
|
||||||
return page.NewCursorKey(ci.ID, ci.Status)
|
|
||||||
case ContinualImprovementOrderFieldPriority:
|
|
||||||
return page.NewCursorKey(ci.ID, ci.Priority)
|
|
||||||
case ContinualImprovementOrderFieldReferenceId:
|
|
||||||
return page.NewCursorKey(ci.ID, ci.ReferenceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported order by: %s", field))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthorizationAttributes returns the authorization attributes for policy evaluation.
|
|
||||||
func (ci *ContinualImprovement) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
|
|
||||||
q := `SELECT organization_id FROM continual_improvements WHERE id = $1 LIMIT 1;`
|
|
||||||
|
|
||||||
var organizationID gid.GID
|
|
||||||
if err := conn.QueryRow(ctx, q, ci.ID).Scan(&organizationID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return nil, ErrResourceNotFound
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cannot query continual improvement authorization attributes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]string{"organization_id": organizationID.String()}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ContinualImprovement) LoadByID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
continualImprovementID gid.GID,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
source,
|
|
||||||
owner_profile_id,
|
|
||||||
target_date,
|
|
||||||
status,
|
|
||||||
priority,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
continual_improvements
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @continual_improvement_id
|
|
||||||
LIMIT 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"continual_improvement_id": continualImprovementID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query continual improvement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
improvement, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ContinualImprovement])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect continual improvement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*ci = improvement
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cis *ContinualImprovements) CountByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
filter *ContinualImprovementFilter,
|
|
||||||
) (int, error) {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
COUNT(id)
|
|
||||||
FROM
|
|
||||||
continual_improvements
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, filter.SQLArguments())
|
|
||||||
|
|
||||||
row := conn.QueryRow(ctx, q, args)
|
|
||||||
|
|
||||||
var count int
|
|
||||||
err := row.Scan(&count)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("cannot count continual improvements: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cis *ContinualImprovements) LoadByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
cursor *page.Cursor[ContinualImprovementOrderField],
|
|
||||||
filter *ContinualImprovementFilter,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
source,
|
|
||||||
owner_profile_id,
|
|
||||||
target_date,
|
|
||||||
status,
|
|
||||||
priority,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
continual_improvements
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND %s
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, filter.SQLArguments())
|
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query continual improvements: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
improvements, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ContinualImprovement])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect continual improvements: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*cis = improvements
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ContinualImprovement) Insert(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
INSERT INTO continual_improvements (
|
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
organization_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
source,
|
|
||||||
owner_profile_id,
|
|
||||||
target_date,
|
|
||||||
status,
|
|
||||||
priority,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES (
|
|
||||||
@id,
|
|
||||||
@tenant_id,
|
|
||||||
@organization_id,
|
|
||||||
@reference_id,
|
|
||||||
@description,
|
|
||||||
@source,
|
|
||||||
@owner_profile_id,
|
|
||||||
@target_date,
|
|
||||||
@status,
|
|
||||||
@priority,
|
|
||||||
@created_at,
|
|
||||||
@updated_at
|
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"id": ci.ID,
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"organization_id": ci.OrganizationID,
|
|
||||||
"reference_id": ci.ReferenceID,
|
|
||||||
"description": ci.Description,
|
|
||||||
"source": ci.Source,
|
|
||||||
"owner_profile_id": ci.OwnerID,
|
|
||||||
"target_date": ci.TargetDate,
|
|
||||||
"status": ci.Status,
|
|
||||||
"priority": ci.Priority,
|
|
||||||
"created_at": ci.CreatedAt,
|
|
||||||
"updated_at": ci.UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert continual improvement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ContinualImprovement) Update(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
UPDATE continual_improvements SET
|
|
||||||
reference_id = @reference_id,
|
|
||||||
description = @description,
|
|
||||||
source = @source,
|
|
||||||
owner_profile_id = @owner_profile_id,
|
|
||||||
target_date = @target_date,
|
|
||||||
status = @status,
|
|
||||||
priority = @priority,
|
|
||||||
updated_at = @updated_at
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @id
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"id": ci.ID,
|
|
||||||
"reference_id": ci.ReferenceID,
|
|
||||||
"description": ci.Description,
|
|
||||||
"source": ci.Source,
|
|
||||||
"owner_profile_id": ci.OwnerID,
|
|
||||||
"target_date": ci.TargetDate,
|
|
||||||
"status": ci.Status,
|
|
||||||
"priority": ci.Priority,
|
|
||||||
"updated_at": ci.UpdatedAt,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot update continual improvement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ContinualImprovement) Delete(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
DELETE FROM continual_improvements
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @id
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"id": ci.ID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot delete continual improvement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cis ContinualImprovements) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error {
|
|
||||||
query := `
|
|
||||||
INSERT INTO continual_improvements (
|
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
source,
|
|
||||||
owner_profile_id,
|
|
||||||
target_date,
|
|
||||||
status,
|
|
||||||
priority,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @continual_improvement_entity_type),
|
|
||||||
@tenant_id,
|
|
||||||
@snapshot_id,
|
|
||||||
r.id,
|
|
||||||
r.organization_id,
|
|
||||||
r.reference_id,
|
|
||||||
r.description,
|
|
||||||
r.source,
|
|
||||||
r.owner_profile_id,
|
|
||||||
r.target_date,
|
|
||||||
r.status,
|
|
||||||
r.priority,
|
|
||||||
r.created_at,
|
|
||||||
r.updated_at
|
|
||||||
FROM continual_improvements r
|
|
||||||
WHERE %s AND r.organization_id = @organization_id AND r.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"continual_improvement_entity_type": ContinualImprovementEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert continual improvement snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -51,11 +51,11 @@ const (
|
|||||||
FileEntityType uint16 = 25
|
FileEntityType uint16 = 25
|
||||||
VendorContactEntityType uint16 = 26
|
VendorContactEntityType uint16 = 26
|
||||||
VendorDataPrivacyAgreementEntityType uint16 = 27
|
VendorDataPrivacyAgreementEntityType uint16 = 27
|
||||||
NonconformityEntityType uint16 = 28
|
_ uint16 = 28 // NonconformityEntityType - removed
|
||||||
ObligationEntityType uint16 = 29
|
ObligationEntityType uint16 = 29
|
||||||
VendorServiceEntityType uint16 = 30
|
VendorServiceEntityType uint16 = 30
|
||||||
SnapshotEntityType uint16 = 31
|
SnapshotEntityType uint16 = 31
|
||||||
ContinualImprovementEntityType uint16 = 32
|
_ uint16 = 32 // ContinualImprovementEntityType - removed
|
||||||
ProcessingActivityEntityType uint16 = 33
|
ProcessingActivityEntityType uint16 = 33
|
||||||
ExportJobEntityType uint16 = 34
|
ExportJobEntityType uint16 = 34
|
||||||
TrustCenterReferenceEntityType uint16 = 35
|
TrustCenterReferenceEntityType uint16 = 35
|
||||||
@@ -90,6 +90,7 @@ const (
|
|||||||
MailingListEntityType uint16 = 64
|
MailingListEntityType uint16 = 64
|
||||||
MailingListSubscriberEntityType uint16 = 65
|
MailingListSubscriberEntityType uint16 = 65
|
||||||
MailingListUpdateEntityType uint16 = 66
|
MailingListUpdateEntityType uint16 = 66
|
||||||
|
FindingEntityType uint16 = 67
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewEntityFromID(id gid.GID) (any, bool) {
|
func NewEntityFromID(id gid.GID) (any, bool) {
|
||||||
@@ -148,16 +149,14 @@ func NewEntityFromID(id gid.GID) (any, bool) {
|
|||||||
return &VendorContact{ID: id}, true
|
return &VendorContact{ID: id}, true
|
||||||
case VendorDataPrivacyAgreementEntityType:
|
case VendorDataPrivacyAgreementEntityType:
|
||||||
return &VendorDataPrivacyAgreement{ID: id}, true
|
return &VendorDataPrivacyAgreement{ID: id}, true
|
||||||
case NonconformityEntityType:
|
case FindingEntityType:
|
||||||
return &Nonconformity{ID: id}, true
|
return &Finding{ID: id}, true
|
||||||
case ObligationEntityType:
|
case ObligationEntityType:
|
||||||
return &Obligation{ID: id}, true
|
return &Obligation{ID: id}, true
|
||||||
case VendorServiceEntityType:
|
case VendorServiceEntityType:
|
||||||
return &VendorService{ID: id}, true
|
return &VendorService{ID: id}, true
|
||||||
case SnapshotEntityType:
|
case SnapshotEntityType:
|
||||||
return &Snapshot{ID: id}, true
|
return &Snapshot{ID: id}, true
|
||||||
case ContinualImprovementEntityType:
|
|
||||||
return &ContinualImprovement{ID: id}, true
|
|
||||||
case ProcessingActivityEntityType:
|
case ProcessingActivityEntityType:
|
||||||
return &ProcessingActivity{ID: id}, true
|
return &ProcessingActivity{ID: id}, true
|
||||||
case ExportJobEntityType:
|
case ExportJobEntityType:
|
||||||
|
|||||||
623
pkg/coredata/finding.go
Normal file
623
pkg/coredata/finding.go
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
// Copyright (c) 2026 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"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Finding struct {
|
||||||
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
|
SnapshotID *gid.GID `db:"snapshot_id"`
|
||||||
|
SourceID *gid.GID `db:"source_id"`
|
||||||
|
Kind FindingKind `db:"kind"`
|
||||||
|
ReferenceID string `db:"reference_id"`
|
||||||
|
Description *string `db:"description"`
|
||||||
|
Source *string `db:"source"`
|
||||||
|
IdentifiedOn *time.Time `db:"identified_on"`
|
||||||
|
RootCause *string `db:"root_cause"`
|
||||||
|
CorrectiveAction *string `db:"corrective_action"`
|
||||||
|
OwnerID *gid.GID `db:"owner_id"`
|
||||||
|
DueDate *time.Time `db:"due_date"`
|
||||||
|
Status FindingStatus `db:"status"`
|
||||||
|
Priority FindingPriority `db:"priority"`
|
||||||
|
RiskID *gid.GID `db:"risk_id"`
|
||||||
|
EffectivenessCheck *string `db:"effectiveness_check"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Findings []*Finding
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *Finding) CursorKey(field FindingOrderField) page.CursorKey {
|
||||||
|
switch field {
|
||||||
|
case FindingOrderFieldCreatedAt:
|
||||||
|
return page.NewCursorKey(f.ID, f.CreatedAt)
|
||||||
|
case FindingOrderFieldIdentifiedOn:
|
||||||
|
return page.NewCursorKey(f.ID, f.IdentifiedOn)
|
||||||
|
case FindingOrderFieldDueDate:
|
||||||
|
return page.NewCursorKey(f.ID, f.DueDate)
|
||||||
|
case FindingOrderFieldStatus:
|
||||||
|
return page.NewCursorKey(f.ID, f.Status)
|
||||||
|
case FindingOrderFieldPriority:
|
||||||
|
return page.NewCursorKey(f.ID, f.Priority)
|
||||||
|
case FindingOrderFieldReferenceId:
|
||||||
|
return page.NewCursorKey(f.ID, f.ReferenceID)
|
||||||
|
case FindingOrderFieldKind:
|
||||||
|
return page.NewCursorKey(f.ID, f.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("unsupported order by: %s", field))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Finding) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
|
||||||
|
q := `SELECT organization_id FROM findings WHERE id = $1 LIMIT 1;`
|
||||||
|
|
||||||
|
var organizationID gid.GID
|
||||||
|
if err := conn.QueryRow(ctx, q, f.ID).Scan(&organizationID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("cannot query finding authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]string{"organization_id": organizationID.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Finding) LoadByID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
findingID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
kind,
|
||||||
|
reference_id,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
identified_on,
|
||||||
|
root_cause,
|
||||||
|
corrective_action,
|
||||||
|
owner_id,
|
||||||
|
due_date,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
risk_id,
|
||||||
|
effectiveness_check,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
findings
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @finding_id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"finding_id": findingID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query finding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
finding, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Finding])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect finding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*f = finding
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *Findings) CountByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *FindingFilter,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
COUNT(id)
|
||||||
|
FROM
|
||||||
|
findings
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count findings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *Findings) LoadByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
cursor *page.Cursor[FindingOrderField],
|
||||||
|
filter *FindingFilter,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
kind,
|
||||||
|
reference_id,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
identified_on,
|
||||||
|
root_cause,
|
||||||
|
corrective_action,
|
||||||
|
owner_id,
|
||||||
|
due_date,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
risk_id,
|
||||||
|
effectiveness_check,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
findings
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query findings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
findings, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Finding])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect findings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*fs = findings
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Finding) Insert(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH next_ref AS (
|
||||||
|
SELECT pg_advisory_xact_lock(hashtext(@organization_id::text)),
|
||||||
|
COALESCE(
|
||||||
|
MAX(CAST(SUBSTRING(reference_id FROM 5) AS INTEGER)),
|
||||||
|
0
|
||||||
|
) + 1 AS next_num
|
||||||
|
FROM findings
|
||||||
|
WHERE organization_id = @organization_id AND snapshot_id IS NULL
|
||||||
|
)
|
||||||
|
INSERT INTO findings (
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
kind,
|
||||||
|
reference_id,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
identified_on,
|
||||||
|
root_cause,
|
||||||
|
corrective_action,
|
||||||
|
owner_id,
|
||||||
|
due_date,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
risk_id,
|
||||||
|
effectiveness_check,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@id,
|
||||||
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
|
@kind,
|
||||||
|
'FND-' || LPAD(next_ref.next_num::TEXT, 3, '0'),
|
||||||
|
@description,
|
||||||
|
@source,
|
||||||
|
@identified_on,
|
||||||
|
@root_cause,
|
||||||
|
@corrective_action,
|
||||||
|
@owner_id,
|
||||||
|
@due_date,
|
||||||
|
@status,
|
||||||
|
@priority,
|
||||||
|
@risk_id,
|
||||||
|
@effectiveness_check,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
FROM next_ref
|
||||||
|
RETURNING reference_id
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": f.ID,
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": f.OrganizationID,
|
||||||
|
"kind": f.Kind,
|
||||||
|
"description": f.Description,
|
||||||
|
"source": f.Source,
|
||||||
|
"identified_on": f.IdentifiedOn,
|
||||||
|
"root_cause": f.RootCause,
|
||||||
|
"corrective_action": f.CorrectiveAction,
|
||||||
|
"owner_id": f.OwnerID,
|
||||||
|
"due_date": f.DueDate,
|
||||||
|
"status": f.Status,
|
||||||
|
"priority": f.Priority,
|
||||||
|
"risk_id": f.RiskID,
|
||||||
|
"effectiveness_check": f.EffectivenessCheck,
|
||||||
|
"created_at": f.CreatedAt,
|
||||||
|
"updated_at": f.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conn.QueryRow(ctx, q, args).Scan(&f.ReferenceID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert finding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Finding) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE findings
|
||||||
|
SET
|
||||||
|
description = @description,
|
||||||
|
source = @source,
|
||||||
|
identified_on = @identified_on,
|
||||||
|
root_cause = @root_cause,
|
||||||
|
corrective_action = @corrective_action,
|
||||||
|
owner_id = @owner_id,
|
||||||
|
due_date = @due_date,
|
||||||
|
status = @status,
|
||||||
|
priority = @priority,
|
||||||
|
risk_id = @risk_id,
|
||||||
|
effectiveness_check = @effectiveness_check,
|
||||||
|
updated_at = @updated_at
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": f.ID,
|
||||||
|
"description": f.Description,
|
||||||
|
"source": f.Source,
|
||||||
|
"identified_on": f.IdentifiedOn,
|
||||||
|
"root_cause": f.RootCause,
|
||||||
|
"corrective_action": f.CorrectiveAction,
|
||||||
|
"owner_id": f.OwnerID,
|
||||||
|
"due_date": f.DueDate,
|
||||||
|
"status": f.Status,
|
||||||
|
"priority": f.Priority,
|
||||||
|
"risk_id": f.RiskID,
|
||||||
|
"effectiveness_check": f.EffectivenessCheck,
|
||||||
|
"updated_at": f.UpdatedAt,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot update finding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Finding) Delete(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
DELETE FROM findings
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id AND snapshot_id IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"id": f.ID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot delete finding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs Findings) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO findings (
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
organization_id,
|
||||||
|
kind,
|
||||||
|
reference_id,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
identified_on,
|
||||||
|
root_cause,
|
||||||
|
corrective_action,
|
||||||
|
owner_id,
|
||||||
|
due_date,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
risk_id,
|
||||||
|
effectiveness_check,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
generate_gid(decode_base64_unpadded(@tenant_id), @finding_entity_type),
|
||||||
|
@tenant_id,
|
||||||
|
@snapshot_id,
|
||||||
|
f.id,
|
||||||
|
f.organization_id,
|
||||||
|
f.kind,
|
||||||
|
f.reference_id,
|
||||||
|
f.description,
|
||||||
|
f.source,
|
||||||
|
f.identified_on,
|
||||||
|
f.root_cause,
|
||||||
|
f.corrective_action,
|
||||||
|
f.owner_id,
|
||||||
|
f.due_date,
|
||||||
|
f.status,
|
||||||
|
f.priority,
|
||||||
|
f.risk_id,
|
||||||
|
f.effectiveness_check,
|
||||||
|
f.created_at,
|
||||||
|
f.updated_at
|
||||||
|
FROM findings f
|
||||||
|
WHERE %s AND f.organization_id = @organization_id AND f.snapshot_id IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"snapshot_id": snapshotID,
|
||||||
|
"organization_id": organizationID,
|
||||||
|
"finding_entity_type": FindingEntityType,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, query, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert finding snapshots: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
auditQuery := `
|
||||||
|
INSERT INTO findings_audits (finding_id, audit_id, reference_id, organization_id, tenant_id, created_at)
|
||||||
|
SELECT
|
||||||
|
snap.id,
|
||||||
|
fa.audit_id,
|
||||||
|
fa.reference_id,
|
||||||
|
fa.organization_id,
|
||||||
|
fa.tenant_id,
|
||||||
|
fa.created_at
|
||||||
|
FROM findings_audits fa
|
||||||
|
JOIN findings live ON fa.finding_id = live.id AND live.snapshot_id IS NULL
|
||||||
|
JOIN findings snap ON snap.source_id = live.id AND snap.snapshot_id = @snapshot_id
|
||||||
|
WHERE %s AND live.organization_id = @organization_id
|
||||||
|
`
|
||||||
|
|
||||||
|
auditQuery = fmt.Sprintf(auditQuery, scope.SQLFragment())
|
||||||
|
|
||||||
|
_, err = conn.Exec(ctx, auditQuery, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert finding audit snapshots: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *Findings) LoadByAuditID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
auditID gid.GID,
|
||||||
|
cursor *page.Cursor[FindingOrderField],
|
||||||
|
filter *FindingFilter,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH f AS (
|
||||||
|
SELECT
|
||||||
|
fi.id,
|
||||||
|
fi.tenant_id,
|
||||||
|
fi.organization_id,
|
||||||
|
fi.snapshot_id,
|
||||||
|
fi.source_id,
|
||||||
|
fi.kind,
|
||||||
|
fi.reference_id,
|
||||||
|
fi.description,
|
||||||
|
fi.source,
|
||||||
|
fi.identified_on,
|
||||||
|
fi.root_cause,
|
||||||
|
fi.corrective_action,
|
||||||
|
fi.owner_id,
|
||||||
|
fi.due_date,
|
||||||
|
fi.status,
|
||||||
|
fi.priority,
|
||||||
|
fi.risk_id,
|
||||||
|
fi.effectiveness_check,
|
||||||
|
fi.created_at,
|
||||||
|
fi.updated_at
|
||||||
|
FROM
|
||||||
|
findings fi
|
||||||
|
INNER JOIN
|
||||||
|
findings_audits fa ON fi.id = fa.finding_id
|
||||||
|
WHERE
|
||||||
|
fa.audit_id = @audit_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
kind,
|
||||||
|
reference_id,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
identified_on,
|
||||||
|
root_cause,
|
||||||
|
corrective_action,
|
||||||
|
owner_id,
|
||||||
|
due_date,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
risk_id,
|
||||||
|
effectiveness_check,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
f
|
||||||
|
WHERE %s
|
||||||
|
AND %s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"audit_id": auditID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query findings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
findings, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Finding])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect findings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*fs = findings
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *Findings) CountByAuditID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
auditID gid.GID,
|
||||||
|
filter *FindingFilter,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
WITH f AS (
|
||||||
|
SELECT
|
||||||
|
fi.id,
|
||||||
|
fi.tenant_id
|
||||||
|
FROM
|
||||||
|
findings fi
|
||||||
|
INNER JOIN
|
||||||
|
findings_audits fa ON fi.id = fa.finding_id
|
||||||
|
WHERE
|
||||||
|
fa.audit_id = @audit_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(id)
|
||||||
|
FROM
|
||||||
|
f
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"audit_id": auditID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count findings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
112
pkg/coredata/finding_audit.go
Normal file
112
pkg/coredata/finding_audit.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) 2026 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"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FindingAudit struct {
|
||||||
|
FindingID gid.GID `db:"finding_id"`
|
||||||
|
AuditID gid.GID `db:"audit_id"`
|
||||||
|
ReferenceID string `db:"reference_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FindingAudits []*FindingAudit
|
||||||
|
)
|
||||||
|
|
||||||
|
func (fa FindingAudit) Upsert(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
INSERT INTO
|
||||||
|
findings_audits (
|
||||||
|
finding_id,
|
||||||
|
audit_id,
|
||||||
|
reference_id,
|
||||||
|
organization_id,
|
||||||
|
tenant_id,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@finding_id,
|
||||||
|
@audit_id,
|
||||||
|
@reference_id,
|
||||||
|
@organization_id,
|
||||||
|
@tenant_id,
|
||||||
|
@created_at
|
||||||
|
)
|
||||||
|
ON CONFLICT (finding_id, audit_id) DO NOTHING;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"finding_id": fa.FindingID,
|
||||||
|
"audit_id": fa.AuditID,
|
||||||
|
"reference_id": fa.ReferenceID,
|
||||||
|
"organization_id": fa.OrganizationID,
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"created_at": fa.CreatedAt,
|
||||||
|
}
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot upsert finding audit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fa FindingAudit) Delete(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
findingID gid.GID,
|
||||||
|
auditID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
findings_audits
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND finding_id = @finding_id
|
||||||
|
AND audit_id = @audit_id;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"finding_id": findingID,
|
||||||
|
"audit_id": auditID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot delete finding audit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
132
pkg/coredata/finding_filter.go
Normal file
132
pkg/coredata/finding_filter.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// Copyright (c) 2026 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 (
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FindingFilter struct {
|
||||||
|
snapshotID **gid.GID
|
||||||
|
kind *FindingKind
|
||||||
|
status *FindingStatus
|
||||||
|
priority *FindingPriority
|
||||||
|
ownerID *gid.GID
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFindingFilter(
|
||||||
|
snapshotID **gid.GID,
|
||||||
|
kind *FindingKind,
|
||||||
|
status *FindingStatus,
|
||||||
|
priority *FindingPriority,
|
||||||
|
ownerID *gid.GID,
|
||||||
|
) *FindingFilter {
|
||||||
|
return &FindingFilter{
|
||||||
|
snapshotID: snapshotID,
|
||||||
|
kind: kind,
|
||||||
|
status: status,
|
||||||
|
priority: priority,
|
||||||
|
ownerID: ownerID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FindingFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"has_snapshot_filter": false,
|
||||||
|
"filter_snapshot_id": nil,
|
||||||
|
"has_kind_filter": false,
|
||||||
|
"filter_kind": nil,
|
||||||
|
"has_status_filter": false,
|
||||||
|
"filter_status": nil,
|
||||||
|
"has_priority_filter": false,
|
||||||
|
"filter_priority": nil,
|
||||||
|
"has_owner_filter": false,
|
||||||
|
"filter_owner_id": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.snapshotID != nil {
|
||||||
|
args["has_snapshot_filter"] = true
|
||||||
|
if *f.snapshotID != nil {
|
||||||
|
args["filter_snapshot_id"] = **f.snapshotID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.kind != nil {
|
||||||
|
args["has_kind_filter"] = true
|
||||||
|
args["filter_kind"] = string(*f.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.status != nil {
|
||||||
|
args["has_status_filter"] = true
|
||||||
|
args["filter_status"] = string(*f.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.priority != nil {
|
||||||
|
args["has_priority_filter"] = true
|
||||||
|
args["filter_priority"] = string(*f.priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.ownerID != nil {
|
||||||
|
args["has_owner_filter"] = true
|
||||||
|
args["filter_owner_id"] = *f.ownerID
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FindingFilter) SQLFragment() string {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
CASE
|
||||||
|
WHEN @has_snapshot_filter::boolean = false THEN TRUE
|
||||||
|
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NOT NULL THEN
|
||||||
|
snapshot_id = @filter_snapshot_id::text
|
||||||
|
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NULL THEN
|
||||||
|
snapshot_id IS NULL
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
AND
|
||||||
|
CASE
|
||||||
|
WHEN @has_kind_filter::boolean = false THEN TRUE
|
||||||
|
WHEN @has_kind_filter::boolean = true THEN
|
||||||
|
kind = @filter_kind::findings_kind
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
AND
|
||||||
|
CASE
|
||||||
|
WHEN @has_status_filter::boolean = false THEN TRUE
|
||||||
|
WHEN @has_status_filter::boolean = true THEN
|
||||||
|
status = @filter_status::findings_status
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
AND
|
||||||
|
CASE
|
||||||
|
WHEN @has_priority_filter::boolean = false THEN TRUE
|
||||||
|
WHEN @has_priority_filter::boolean = true THEN
|
||||||
|
priority = @filter_priority::findings_priority
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
AND
|
||||||
|
CASE
|
||||||
|
WHEN @has_owner_filter::boolean = false THEN TRUE
|
||||||
|
WHEN @has_owner_filter::boolean = true THEN
|
||||||
|
owner_id = @filter_owner_id::text
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)`
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -19,27 +19,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NonconformityStatus string
|
type FindingKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NonconformityStatusOpen NonconformityStatus = "OPEN"
|
FindingKindNonconformity FindingKind = "NONCONFORMITY"
|
||||||
NonconformityStatusInProgress NonconformityStatus = "IN_PROGRESS"
|
FindingKindObservation FindingKind = "OBSERVATION"
|
||||||
NonconformityStatusClosed NonconformityStatus = "CLOSED"
|
FindingKindException FindingKind = "EXCEPTION"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NonconformityStatuses() []NonconformityStatus {
|
func FindingKinds() []FindingKind {
|
||||||
return []NonconformityStatus{
|
return []FindingKind{
|
||||||
NonconformityStatusOpen,
|
FindingKindNonconformity,
|
||||||
NonconformityStatusInProgress,
|
FindingKindObservation,
|
||||||
NonconformityStatusClosed,
|
FindingKindException,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ncs NonconformityStatus) String() string {
|
func (fk FindingKind) String() string {
|
||||||
return string(ncs)
|
return string(fk)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ncs *NonconformityStatus) Scan(value any) error {
|
func (fk *FindingKind) Scan(value any) error {
|
||||||
var s string
|
var s string
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -47,22 +47,22 @@ func (ncs *NonconformityStatus) Scan(value any) error {
|
|||||||
case []byte:
|
case []byte:
|
||||||
s = string(v)
|
s = string(v)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported type for NonconformityStatus: %T", value)
|
return fmt.Errorf("unsupported type for FindingKind: %T", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case "OPEN":
|
case "NONCONFORMITY":
|
||||||
*ncs = NonconformityStatusOpen
|
*fk = FindingKindNonconformity
|
||||||
case "IN_PROGRESS":
|
case "OBSERVATION":
|
||||||
*ncs = NonconformityStatusInProgress
|
*fk = FindingKindObservation
|
||||||
case "CLOSED":
|
case "EXCEPTION":
|
||||||
*ncs = NonconformityStatusClosed
|
*fk = FindingKindException
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid NonconformityStatus value: %q", s)
|
return fmt.Errorf("invalid FindingKind value: %q", s)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ncs NonconformityStatus) Value() (driver.Value, error) {
|
func (fk FindingKind) Value() (driver.Value, error) {
|
||||||
return ncs.String(), nil
|
return fk.String(), nil
|
||||||
}
|
}
|
||||||
59
pkg/coredata/finding_order_field.go
Normal file
59
pkg/coredata/finding_order_field.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) 2026 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 (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FindingOrderField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FindingOrderFieldCreatedAt FindingOrderField = "CREATED_AT"
|
||||||
|
FindingOrderFieldIdentifiedOn FindingOrderField = "IDENTIFIED_ON"
|
||||||
|
FindingOrderFieldDueDate FindingOrderField = "DUE_DATE"
|
||||||
|
FindingOrderFieldStatus FindingOrderField = "STATUS"
|
||||||
|
FindingOrderFieldPriority FindingOrderField = "PRIORITY"
|
||||||
|
FindingOrderFieldReferenceId FindingOrderField = "REFERENCE_ID"
|
||||||
|
FindingOrderFieldKind FindingOrderField = "KIND"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p FindingOrderField) Column() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p FindingOrderField) String() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p FindingOrderField) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(p.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FindingOrderField) UnmarshalText(text []byte) error {
|
||||||
|
val := string(text)
|
||||||
|
switch val {
|
||||||
|
case string(FindingOrderFieldCreatedAt),
|
||||||
|
string(FindingOrderFieldIdentifiedOn),
|
||||||
|
string(FindingOrderFieldDueDate),
|
||||||
|
string(FindingOrderFieldStatus),
|
||||||
|
string(FindingOrderFieldPriority),
|
||||||
|
string(FindingOrderFieldReferenceId),
|
||||||
|
string(FindingOrderFieldKind):
|
||||||
|
*p = FindingOrderField(val)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid FindingOrderField value: %q", val)
|
||||||
|
}
|
||||||
68
pkg/coredata/finding_priority.go
Normal file
68
pkg/coredata/finding_priority.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (c) 2026 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 FindingPriority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FindingPriorityLow FindingPriority = "LOW"
|
||||||
|
FindingPriorityMedium FindingPriority = "MEDIUM"
|
||||||
|
FindingPriorityHigh FindingPriority = "HIGH"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindingPriorities() []FindingPriority {
|
||||||
|
return []FindingPriority{
|
||||||
|
FindingPriorityLow,
|
||||||
|
FindingPriorityMedium,
|
||||||
|
FindingPriorityHigh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp FindingPriority) String() string {
|
||||||
|
return string(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FindingPriority) Scan(value any) error {
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type for FindingPriority: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case "LOW":
|
||||||
|
*fp = FindingPriorityLow
|
||||||
|
case "MEDIUM":
|
||||||
|
*fp = FindingPriorityMedium
|
||||||
|
case "HIGH":
|
||||||
|
*fp = FindingPriorityHigh
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid FindingPriority value: %q", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp FindingPriority) Value() (driver.Value, error) {
|
||||||
|
return fp.String(), nil
|
||||||
|
}
|
||||||
80
pkg/coredata/finding_status.go
Normal file
80
pkg/coredata/finding_status.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (c) 2026 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 FindingStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FindingStatusOpen FindingStatus = "OPEN"
|
||||||
|
FindingStatusInProgress FindingStatus = "IN_PROGRESS"
|
||||||
|
FindingStatusClosed FindingStatus = "CLOSED"
|
||||||
|
FindingStatusRiskAccepted FindingStatus = "RISK_ACCEPTED"
|
||||||
|
FindingStatusMitigated FindingStatus = "MITIGATED"
|
||||||
|
FindingStatusFalsePositive FindingStatus = "FALSE_POSITIVE"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindingStatuses() []FindingStatus {
|
||||||
|
return []FindingStatus{
|
||||||
|
FindingStatusOpen,
|
||||||
|
FindingStatusInProgress,
|
||||||
|
FindingStatusClosed,
|
||||||
|
FindingStatusRiskAccepted,
|
||||||
|
FindingStatusMitigated,
|
||||||
|
FindingStatusFalsePositive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs FindingStatus) String() string {
|
||||||
|
return string(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FindingStatus) Scan(value any) error {
|
||||||
|
var s string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
s = v
|
||||||
|
case []byte:
|
||||||
|
s = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type for FindingStatus: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case "OPEN":
|
||||||
|
*fs = FindingStatusOpen
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
*fs = FindingStatusInProgress
|
||||||
|
case "CLOSED":
|
||||||
|
*fs = FindingStatusClosed
|
||||||
|
case "RISK_ACCEPTED":
|
||||||
|
*fs = FindingStatusRiskAccepted
|
||||||
|
case "MITIGATED":
|
||||||
|
*fs = FindingStatusMitigated
|
||||||
|
case "FALSE_POSITIVE":
|
||||||
|
*fs = FindingStatusFalsePositive
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid FindingStatus value: %q", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs FindingStatus) Value() (driver.Value, error) {
|
||||||
|
return fs.String(), nil
|
||||||
|
}
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Nonconformity struct {
|
|
||||||
ID gid.GID `db:"id"`
|
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
ReferenceID string `db:"reference_id"`
|
|
||||||
Description *string `db:"description"`
|
|
||||||
AuditID *gid.GID `db:"audit_id"`
|
|
||||||
DateIdentified *time.Time `db:"date_identified"`
|
|
||||||
RootCause string `db:"root_cause"`
|
|
||||||
CorrectiveAction *string `db:"corrective_action"`
|
|
||||||
OwnerID gid.GID `db:"owner_profile_id"`
|
|
||||||
DueDate *time.Time `db:"due_date"`
|
|
||||||
Status NonconformityStatus `db:"status"`
|
|
||||||
EffectivenessCheck *string `db:"effectiveness_check"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Nonconformities []*Nonconformity
|
|
||||||
)
|
|
||||||
|
|
||||||
func (nc *Nonconformity) CursorKey(field NonconformityOrderField) page.CursorKey {
|
|
||||||
switch field {
|
|
||||||
case NonconformityOrderFieldCreatedAt:
|
|
||||||
return page.NewCursorKey(nc.ID, nc.CreatedAt)
|
|
||||||
case NonconformityOrderFieldDateIdentified:
|
|
||||||
return page.NewCursorKey(nc.ID, nc.DateIdentified)
|
|
||||||
case NonconformityOrderFieldDueDate:
|
|
||||||
return page.NewCursorKey(nc.ID, nc.DueDate)
|
|
||||||
case NonconformityOrderFieldStatus:
|
|
||||||
return page.NewCursorKey(nc.ID, nc.Status)
|
|
||||||
case NonconformityOrderFieldReferenceId:
|
|
||||||
return page.NewCursorKey(nc.ID, nc.ReferenceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported order by: %s", field))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *Nonconformity) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
|
|
||||||
q := `SELECT organization_id FROM nonconformities WHERE id = $1 LIMIT 1;`
|
|
||||||
|
|
||||||
var organizationID gid.GID
|
|
||||||
if err := conn.QueryRow(ctx, q, nc.ID).Scan(&organizationID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return nil, ErrResourceNotFound
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cannot query nonconformity authorization attributes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]string{"organization_id": organizationID.String()}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *Nonconformity) LoadByID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
nonconformityID gid.GID,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
audit_id,
|
|
||||||
date_identified,
|
|
||||||
root_cause,
|
|
||||||
corrective_action,
|
|
||||||
owner_profile_id,
|
|
||||||
due_date,
|
|
||||||
status,
|
|
||||||
effectiveness_check,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
nonconformities
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @nonconformity_id
|
|
||||||
LIMIT 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"nonconformity_id": nonconformityID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query nonconformity: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonconformity, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Nonconformity])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect nonconformity: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*nc = nonconformity
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ncs *Nonconformities) CountByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
filter *NonconformityFilter,
|
|
||||||
) (int, error) {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
COUNT(id)
|
|
||||||
FROM
|
|
||||||
nonconformities
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, filter.SQLArguments())
|
|
||||||
|
|
||||||
row := conn.QueryRow(ctx, q, args)
|
|
||||||
|
|
||||||
var count int
|
|
||||||
err := row.Scan(&count)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("cannot count nonconformities: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ncs *Nonconformities) LoadByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
cursor *page.Cursor[NonconformityOrderField],
|
|
||||||
filter *NonconformityFilter,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
audit_id,
|
|
||||||
date_identified,
|
|
||||||
root_cause,
|
|
||||||
corrective_action,
|
|
||||||
owner_profile_id,
|
|
||||||
due_date,
|
|
||||||
status,
|
|
||||||
effectiveness_check,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
nonconformities
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND %s
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, filter.SQLArguments())
|
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query nonconformities: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonconformities, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Nonconformity])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect nonconformities: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*ncs = nonconformities
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *Nonconformity) Insert(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
INSERT INTO nonconformities (
|
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
organization_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
audit_id,
|
|
||||||
date_identified,
|
|
||||||
root_cause,
|
|
||||||
corrective_action,
|
|
||||||
owner_profile_id,
|
|
||||||
due_date,
|
|
||||||
status,
|
|
||||||
effectiveness_check,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES (
|
|
||||||
@id,
|
|
||||||
@tenant_id,
|
|
||||||
@organization_id,
|
|
||||||
@reference_id,
|
|
||||||
@description,
|
|
||||||
@audit_id,
|
|
||||||
@date_identified,
|
|
||||||
@root_cause,
|
|
||||||
@corrective_action,
|
|
||||||
@owner_profile_id,
|
|
||||||
@due_date,
|
|
||||||
@status,
|
|
||||||
@effectiveness_check,
|
|
||||||
@created_at,
|
|
||||||
@updated_at
|
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"id": nc.ID,
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"organization_id": nc.OrganizationID,
|
|
||||||
"reference_id": nc.ReferenceID,
|
|
||||||
"description": nc.Description,
|
|
||||||
"audit_id": nc.AuditID,
|
|
||||||
"date_identified": nc.DateIdentified,
|
|
||||||
"root_cause": nc.RootCause,
|
|
||||||
"corrective_action": nc.CorrectiveAction,
|
|
||||||
"owner_profile_id": nc.OwnerID,
|
|
||||||
"due_date": nc.DueDate,
|
|
||||||
"status": nc.Status,
|
|
||||||
"effectiveness_check": nc.EffectivenessCheck,
|
|
||||||
"created_at": nc.CreatedAt,
|
|
||||||
"updated_at": nc.UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert nonconformity: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *Nonconformity) Update(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
UPDATE nonconformities
|
|
||||||
SET
|
|
||||||
reference_id = @reference_id,
|
|
||||||
description = @description,
|
|
||||||
date_identified = @date_identified,
|
|
||||||
root_cause = @root_cause,
|
|
||||||
corrective_action = @corrective_action,
|
|
||||||
due_date = @due_date,
|
|
||||||
status = @status,
|
|
||||||
effectiveness_check = @effectiveness_check,
|
|
||||||
owner_profile_id = @owner_profile_id,
|
|
||||||
audit_id = @audit_id,
|
|
||||||
updated_at = @updated_at
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @id
|
|
||||||
AND snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"id": nc.ID,
|
|
||||||
"reference_id": nc.ReferenceID,
|
|
||||||
"description": nc.Description,
|
|
||||||
"date_identified": nc.DateIdentified,
|
|
||||||
"root_cause": nc.RootCause,
|
|
||||||
"corrective_action": nc.CorrectiveAction,
|
|
||||||
"due_date": nc.DueDate,
|
|
||||||
"status": nc.Status,
|
|
||||||
"effectiveness_check": nc.EffectivenessCheck,
|
|
||||||
"owner_profile_id": nc.OwnerID,
|
|
||||||
"audit_id": nc.AuditID,
|
|
||||||
"updated_at": nc.UpdatedAt,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot update nonconformity: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (nc *Nonconformity) Delete(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
DELETE FROM nonconformities
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @id AND snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"id": nc.ID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot delete nonconformity: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ncs Nonconformities) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error {
|
|
||||||
query := `
|
|
||||||
INSERT INTO nonconformities (
|
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
|
||||||
reference_id,
|
|
||||||
description,
|
|
||||||
audit_id,
|
|
||||||
date_identified,
|
|
||||||
root_cause,
|
|
||||||
corrective_action,
|
|
||||||
owner_profile_id,
|
|
||||||
due_date,
|
|
||||||
status,
|
|
||||||
effectiveness_check,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @nonconformity_entity_type),
|
|
||||||
@tenant_id,
|
|
||||||
@snapshot_id,
|
|
||||||
nc.id,
|
|
||||||
nc.organization_id,
|
|
||||||
nc.reference_id,
|
|
||||||
nc.description,
|
|
||||||
nc.audit_id,
|
|
||||||
nc.date_identified,
|
|
||||||
nc.root_cause,
|
|
||||||
nc.corrective_action,
|
|
||||||
nc.owner_profile_id,
|
|
||||||
nc.due_date,
|
|
||||||
nc.status,
|
|
||||||
nc.effectiveness_check,
|
|
||||||
nc.created_at,
|
|
||||||
nc.updated_at
|
|
||||||
FROM nonconformities nc
|
|
||||||
WHERE %s AND nc.organization_id = @organization_id AND nc.snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"nonconformity_entity_type": NonconformityEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert data snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
NonconformityFilter struct {
|
|
||||||
snapshotID **gid.GID
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewNonconformityFilter(snapshotID **gid.GID) *NonconformityFilter {
|
|
||||||
return &NonconformityFilter{
|
|
||||||
snapshotID: snapshotID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *NonconformityFilter) SQLArguments() pgx.NamedArgs {
|
|
||||||
args := pgx.NamedArgs{}
|
|
||||||
|
|
||||||
if f.snapshotID != nil && *f.snapshotID != nil {
|
|
||||||
args["filter_snapshot_id"] = **f.snapshotID
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *NonconformityFilter) SQLFragment() string {
|
|
||||||
if f.snapshotID == nil {
|
|
||||||
return "TRUE"
|
|
||||||
}
|
|
||||||
|
|
||||||
if *f.snapshotID == nil {
|
|
||||||
return "snapshot_id IS NULL"
|
|
||||||
} else {
|
|
||||||
return "snapshot_id = @filter_snapshot_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NonconformityOrderField string
|
|
||||||
|
|
||||||
const (
|
|
||||||
NonconformityOrderFieldCreatedAt NonconformityOrderField = "CREATED_AT"
|
|
||||||
NonconformityOrderFieldDateIdentified NonconformityOrderField = "DATE_IDENTIFIED"
|
|
||||||
NonconformityOrderFieldDueDate NonconformityOrderField = "DUE_DATE"
|
|
||||||
NonconformityOrderFieldStatus NonconformityOrderField = "STATUS"
|
|
||||||
NonconformityOrderFieldReferenceId NonconformityOrderField = "REFERENCE_ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p NonconformityOrderField) Column() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p NonconformityOrderField) String() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p NonconformityOrderField) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(p.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *NonconformityOrderField) UnmarshalText(text []byte) error {
|
|
||||||
val := string(text)
|
|
||||||
switch val {
|
|
||||||
case string(NonconformityOrderFieldCreatedAt),
|
|
||||||
string(NonconformityOrderFieldDateIdentified),
|
|
||||||
string(NonconformityOrderFieldDueDate),
|
|
||||||
string(NonconformityOrderFieldStatus),
|
|
||||||
string(NonconformityOrderFieldReferenceId):
|
|
||||||
*p = NonconformityOrderField(val)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("invalid NonconformityOrderField value: %q", val)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -28,9 +28,8 @@ const (
|
|||||||
SnapshotsTypeVendors SnapshotsType = "VENDORS"
|
SnapshotsTypeVendors SnapshotsType = "VENDORS"
|
||||||
SnapshotsTypeAssets SnapshotsType = "ASSETS"
|
SnapshotsTypeAssets SnapshotsType = "ASSETS"
|
||||||
SnapshotsTypeData SnapshotsType = "DATA"
|
SnapshotsTypeData SnapshotsType = "DATA"
|
||||||
SnapshotsTypeNonconformities SnapshotsType = "NONCONFORMITIES"
|
SnapshotsTypeFindings SnapshotsType = "FINDINGS"
|
||||||
SnapshotsTypeObligations SnapshotsType = "OBLIGATIONS"
|
SnapshotsTypeObligations SnapshotsType = "OBLIGATIONS"
|
||||||
SnapshotsTypeContinualImprovements SnapshotsType = "CONTINUAL_IMPROVEMENTS"
|
|
||||||
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
|
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
|
||||||
SnapshotsTypeStatesOfApplicability SnapshotsType = "STATES_OF_APPLICABILITY"
|
SnapshotsTypeStatesOfApplicability SnapshotsType = "STATES_OF_APPLICABILITY"
|
||||||
)
|
)
|
||||||
@@ -41,9 +40,8 @@ func SnapshotsTypes() []SnapshotsType {
|
|||||||
SnapshotsTypeVendors,
|
SnapshotsTypeVendors,
|
||||||
SnapshotsTypeAssets,
|
SnapshotsTypeAssets,
|
||||||
SnapshotsTypeData,
|
SnapshotsTypeData,
|
||||||
SnapshotsTypeNonconformities,
|
SnapshotsTypeFindings,
|
||||||
SnapshotsTypeObligations,
|
SnapshotsTypeObligations,
|
||||||
SnapshotsTypeContinualImprovements,
|
|
||||||
SnapshotsTypeProcessingActivities,
|
SnapshotsTypeProcessingActivities,
|
||||||
SnapshotsTypeStatesOfApplicability,
|
SnapshotsTypeStatesOfApplicability,
|
||||||
}
|
}
|
||||||
@@ -73,12 +71,10 @@ func (st *SnapshotsType) Scan(value any) error {
|
|||||||
*st = SnapshotsTypeAssets
|
*st = SnapshotsTypeAssets
|
||||||
case SnapshotsTypeData.String():
|
case SnapshotsTypeData.String():
|
||||||
*st = SnapshotsTypeData
|
*st = SnapshotsTypeData
|
||||||
case SnapshotsTypeNonconformities.String():
|
case SnapshotsTypeFindings.String(), "NONCONFORMITIES", "CONTINUAL_IMPROVEMENTS":
|
||||||
*st = SnapshotsTypeNonconformities
|
*st = SnapshotsTypeFindings
|
||||||
case SnapshotsTypeObligations.String():
|
case SnapshotsTypeObligations.String():
|
||||||
*st = SnapshotsTypeObligations
|
*st = SnapshotsTypeObligations
|
||||||
case SnapshotsTypeContinualImprovements.String():
|
|
||||||
*st = SnapshotsTypeContinualImprovements
|
|
||||||
case SnapshotsTypeProcessingActivities.String():
|
case SnapshotsTypeProcessingActivities.String():
|
||||||
*st = SnapshotsTypeProcessingActivities
|
*st = SnapshotsTypeProcessingActivities
|
||||||
case SnapshotsTypeStatesOfApplicability.String():
|
case SnapshotsTypeStatesOfApplicability.String():
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -34,12 +34,10 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
|
|||||||
return Risks{}, nil
|
return Risks{}, nil
|
||||||
case SnapshotsTypeData:
|
case SnapshotsTypeData:
|
||||||
return Data{}, nil
|
return Data{}, nil
|
||||||
case SnapshotsTypeNonconformities:
|
case SnapshotsTypeFindings:
|
||||||
return Nonconformities{}, nil
|
return Findings{}, nil
|
||||||
case SnapshotsTypeObligations:
|
case SnapshotsTypeObligations:
|
||||||
return Obligations{}, nil
|
return Obligations{}, nil
|
||||||
case SnapshotsTypeContinualImprovements:
|
|
||||||
return ContinualImprovements{}, nil
|
|
||||||
case SnapshotsTypeProcessingActivities:
|
case SnapshotsTypeProcessingActivities:
|
||||||
return ProcessingActivities{}, nil
|
return ProcessingActivities{}, nil
|
||||||
case SnapshotsTypeVendors:
|
case SnapshotsTypeVendors:
|
||||||
|
|||||||
Reference in New Issue
Block a user