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:
Bryan Frimin
2026-03-17 00:16:32 +01:00
parent 736436e997
commit bf276cd3db
19 changed files with 1262 additions and 1227 deletions

View File

@@ -452,6 +452,157 @@ WHERE %s
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(
ctx context.Context,
conn pg.Conn,

View File

@@ -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"
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
// purpose with or without fee is hereby granted, provided that the above
@@ -51,11 +51,11 @@ const (
FileEntityType uint16 = 25
VendorContactEntityType uint16 = 26
VendorDataPrivacyAgreementEntityType uint16 = 27
NonconformityEntityType uint16 = 28
_ uint16 = 28 // NonconformityEntityType - removed
ObligationEntityType uint16 = 29
VendorServiceEntityType uint16 = 30
SnapshotEntityType uint16 = 31
ContinualImprovementEntityType uint16 = 32
_ uint16 = 32 // ContinualImprovementEntityType - removed
ProcessingActivityEntityType uint16 = 33
ExportJobEntityType uint16 = 34
TrustCenterReferenceEntityType uint16 = 35
@@ -90,6 +90,7 @@ const (
MailingListEntityType uint16 = 64
MailingListSubscriberEntityType uint16 = 65
MailingListUpdateEntityType uint16 = 66
FindingEntityType uint16 = 67
)
func NewEntityFromID(id gid.GID) (any, bool) {
@@ -148,16 +149,14 @@ func NewEntityFromID(id gid.GID) (any, bool) {
return &VendorContact{ID: id}, true
case VendorDataPrivacyAgreementEntityType:
return &VendorDataPrivacyAgreement{ID: id}, true
case NonconformityEntityType:
return &Nonconformity{ID: id}, true
case FindingEntityType:
return &Finding{ID: id}, true
case ObligationEntityType:
return &Obligation{ID: id}, true
case VendorServiceEntityType:
return &VendorService{ID: id}, true
case SnapshotEntityType:
return &Snapshot{ID: id}, true
case ContinualImprovementEntityType:
return &ContinualImprovement{ID: id}, true
case ProcessingActivityEntityType:
return &ProcessingActivity{ID: id}, true
case ExportJobEntityType:

623
pkg/coredata/finding.go Normal file
View 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
}

View 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
}

View 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
)`
}

View File

@@ -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
// purpose with or without fee is hereby granted, provided that the above
@@ -19,27 +19,27 @@ import (
"fmt"
)
type NonconformityStatus string
type FindingKind string
const (
NonconformityStatusOpen NonconformityStatus = "OPEN"
NonconformityStatusInProgress NonconformityStatus = "IN_PROGRESS"
NonconformityStatusClosed NonconformityStatus = "CLOSED"
FindingKindNonconformity FindingKind = "NONCONFORMITY"
FindingKindObservation FindingKind = "OBSERVATION"
FindingKindException FindingKind = "EXCEPTION"
)
func NonconformityStatuses() []NonconformityStatus {
return []NonconformityStatus{
NonconformityStatusOpen,
NonconformityStatusInProgress,
NonconformityStatusClosed,
func FindingKinds() []FindingKind {
return []FindingKind{
FindingKindNonconformity,
FindingKindObservation,
FindingKindException,
}
}
func (ncs NonconformityStatus) String() string {
return string(ncs)
func (fk FindingKind) String() string {
return string(fk)
}
func (ncs *NonconformityStatus) Scan(value any) error {
func (fk *FindingKind) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
@@ -47,22 +47,22 @@ func (ncs *NonconformityStatus) Scan(value any) error {
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for NonconformityStatus: %T", value)
return fmt.Errorf("unsupported type for FindingKind: %T", value)
}
switch s {
case "OPEN":
*ncs = NonconformityStatusOpen
case "IN_PROGRESS":
*ncs = NonconformityStatusInProgress
case "CLOSED":
*ncs = NonconformityStatusClosed
case "NONCONFORMITY":
*fk = FindingKindNonconformity
case "OBSERVATION":
*fk = FindingKindObservation
case "EXCEPTION":
*fk = FindingKindException
default:
return fmt.Errorf("invalid NonconformityStatus value: %q", s)
return fmt.Errorf("invalid FindingKind value: %q", s)
}
return nil
}
func (ncs NonconformityStatus) Value() (driver.Value, error) {
return ncs.String(), nil
func (fk FindingKind) Value() (driver.Value, error) {
return fk.String(), nil
}

View 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)
}

View 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
}

View 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
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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)
}

View File

@@ -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
// purpose with or without fee is hereby granted, provided that the above
@@ -28,9 +28,8 @@ const (
SnapshotsTypeVendors SnapshotsType = "VENDORS"
SnapshotsTypeAssets SnapshotsType = "ASSETS"
SnapshotsTypeData SnapshotsType = "DATA"
SnapshotsTypeNonconformities SnapshotsType = "NONCONFORMITIES"
SnapshotsTypeFindings SnapshotsType = "FINDINGS"
SnapshotsTypeObligations SnapshotsType = "OBLIGATIONS"
SnapshotsTypeContinualImprovements SnapshotsType = "CONTINUAL_IMPROVEMENTS"
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
SnapshotsTypeStatesOfApplicability SnapshotsType = "STATES_OF_APPLICABILITY"
)
@@ -41,9 +40,8 @@ func SnapshotsTypes() []SnapshotsType {
SnapshotsTypeVendors,
SnapshotsTypeAssets,
SnapshotsTypeData,
SnapshotsTypeNonconformities,
SnapshotsTypeFindings,
SnapshotsTypeObligations,
SnapshotsTypeContinualImprovements,
SnapshotsTypeProcessingActivities,
SnapshotsTypeStatesOfApplicability,
}
@@ -73,12 +71,10 @@ func (st *SnapshotsType) Scan(value any) error {
*st = SnapshotsTypeAssets
case SnapshotsTypeData.String():
*st = SnapshotsTypeData
case SnapshotsTypeNonconformities.String():
*st = SnapshotsTypeNonconformities
case SnapshotsTypeFindings.String(), "NONCONFORMITIES", "CONTINUAL_IMPROVEMENTS":
*st = SnapshotsTypeFindings
case SnapshotsTypeObligations.String():
*st = SnapshotsTypeObligations
case SnapshotsTypeContinualImprovements.String():
*st = SnapshotsTypeContinualImprovements
case SnapshotsTypeProcessingActivities.String():
*st = SnapshotsTypeProcessingActivities
case SnapshotsTypeStatesOfApplicability.String():

View File

@@ -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
// 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
case SnapshotsTypeData:
return Data{}, nil
case SnapshotsTypeNonconformities:
return Nonconformities{}, nil
case SnapshotsTypeFindings:
return Findings{}, nil
case SnapshotsTypeObligations:
return Obligations{}, nil
case SnapshotsTypeContinualImprovements:
return ContinualImprovements{}, nil
case SnapshotsTypeProcessingActivities:
return ProcessingActivities{}, nil
case SnapshotsTypeVendors: