From bf276cd3db7b723a330d5b58e7d4538c11b1a091 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Tue, 17 Mar 2026 00:16:32 +0100 Subject: [PATCH] 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 --- pkg/coredata/audit.go | 151 +++++ pkg/coredata/continual_improvement_filter.go | 54 -- .../continual_improvement_order_field.go | 55 -- .../continual_improvement_priority.go | 68 -- pkg/coredata/continual_improvement_status.go | 68 -- pkg/coredata/continual_improvements.go | 399 ----------- pkg/coredata/entity_type_reg.go | 13 +- pkg/coredata/finding.go | 623 ++++++++++++++++++ pkg/coredata/finding_audit.go | 112 ++++ pkg/coredata/finding_filter.go | 132 ++++ ...onconformity_status.go => finding_kind.go} | 46 +- pkg/coredata/finding_order_field.go | 59 ++ pkg/coredata/finding_priority.go | 68 ++ pkg/coredata/finding_status.go | 80 +++ pkg/coredata/nonconformity.go | 430 ------------ pkg/coredata/nonconformity_filter.go | 54 -- pkg/coredata/nonconformity_order_field.go | 55 -- pkg/coredata/snapshots_type.go | 14 +- pkg/coredata/snapshottable.go | 8 +- 19 files changed, 1262 insertions(+), 1227 deletions(-) delete mode 100644 pkg/coredata/continual_improvement_filter.go delete mode 100644 pkg/coredata/continual_improvement_order_field.go delete mode 100644 pkg/coredata/continual_improvement_priority.go delete mode 100644 pkg/coredata/continual_improvement_status.go delete mode 100644 pkg/coredata/continual_improvements.go create mode 100644 pkg/coredata/finding.go create mode 100644 pkg/coredata/finding_audit.go create mode 100644 pkg/coredata/finding_filter.go rename pkg/coredata/{nonconformity_status.go => finding_kind.go} (50%) create mode 100644 pkg/coredata/finding_order_field.go create mode 100644 pkg/coredata/finding_priority.go create mode 100644 pkg/coredata/finding_status.go delete mode 100644 pkg/coredata/nonconformity.go delete mode 100644 pkg/coredata/nonconformity_filter.go delete mode 100644 pkg/coredata/nonconformity_order_field.go diff --git a/pkg/coredata/audit.go b/pkg/coredata/audit.go index d106969f8..62a0893c7 100644 --- a/pkg/coredata/audit.go +++ b/pkg/coredata/audit.go @@ -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, diff --git a/pkg/coredata/continual_improvement_filter.go b/pkg/coredata/continual_improvement_filter.go deleted file mode 100644 index 862e1e358..000000000 --- a/pkg/coredata/continual_improvement_filter.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "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" - } -} diff --git a/pkg/coredata/continual_improvement_order_field.go b/pkg/coredata/continual_improvement_order_field.go deleted file mode 100644 index cbdb0402c..000000000 --- a/pkg/coredata/continual_improvement_order_field.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "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) -} diff --git a/pkg/coredata/continual_improvement_priority.go b/pkg/coredata/continual_improvement_priority.go deleted file mode 100644 index 0e7e771a1..000000000 --- a/pkg/coredata/continual_improvement_priority.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "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 -} diff --git a/pkg/coredata/continual_improvement_status.go b/pkg/coredata/continual_improvement_status.go deleted file mode 100644 index ff6453bcd..000000000 --- a/pkg/coredata/continual_improvement_status.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "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 -} diff --git a/pkg/coredata/continual_improvements.go b/pkg/coredata/continual_improvements.go deleted file mode 100644 index cc2644823..000000000 --- a/pkg/coredata/continual_improvements.go +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "context" - "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 -} diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index bdeb5074e..6e0e462f3 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -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: diff --git a/pkg/coredata/finding.go b/pkg/coredata/finding.go new file mode 100644 index 000000000..592989739 --- /dev/null +++ b/pkg/coredata/finding.go @@ -0,0 +1,623 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "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 +} diff --git a/pkg/coredata/finding_audit.go b/pkg/coredata/finding_audit.go new file mode 100644 index 000000000..4741ec92e --- /dev/null +++ b/pkg/coredata/finding_audit.go @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "fmt" + "maps" + "time" + + "github.com/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 +} diff --git a/pkg/coredata/finding_filter.go b/pkg/coredata/finding_filter.go new file mode 100644 index 000000000..f81f4e628 --- /dev/null +++ b/pkg/coredata/finding_filter.go @@ -0,0 +1,132 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "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 +)` +} diff --git a/pkg/coredata/nonconformity_status.go b/pkg/coredata/finding_kind.go similarity index 50% rename from pkg/coredata/nonconformity_status.go rename to pkg/coredata/finding_kind.go index 73e226960..a740a11d1 100644 --- a/pkg/coredata/nonconformity_status.go +++ b/pkg/coredata/finding_kind.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -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 } diff --git a/pkg/coredata/finding_order_field.go b/pkg/coredata/finding_order_field.go new file mode 100644 index 000000000..2818b45b7 --- /dev/null +++ b/pkg/coredata/finding_order_field.go @@ -0,0 +1,59 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "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) +} diff --git a/pkg/coredata/finding_priority.go b/pkg/coredata/finding_priority.go new file mode 100644 index 000000000..a62e36094 --- /dev/null +++ b/pkg/coredata/finding_priority.go @@ -0,0 +1,68 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "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 +} diff --git a/pkg/coredata/finding_status.go b/pkg/coredata/finding_status.go new file mode 100644 index 000000000..78913d862 --- /dev/null +++ b/pkg/coredata/finding_status.go @@ -0,0 +1,80 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "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 +} diff --git a/pkg/coredata/nonconformity.go b/pkg/coredata/nonconformity.go deleted file mode 100644 index f6d8db175..000000000 --- a/pkg/coredata/nonconformity.go +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "context" - "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 -} diff --git a/pkg/coredata/nonconformity_filter.go b/pkg/coredata/nonconformity_filter.go deleted file mode 100644 index ac79c007f..000000000 --- a/pkg/coredata/nonconformity_filter.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "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" - } -} diff --git a/pkg/coredata/nonconformity_order_field.go b/pkg/coredata/nonconformity_order_field.go deleted file mode 100644 index ce1c30bc6..000000000 --- a/pkg/coredata/nonconformity_order_field.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "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) -} diff --git a/pkg/coredata/snapshots_type.go b/pkg/coredata/snapshots_type.go index 65d578cb6..bbdd78bc7 100644 --- a/pkg/coredata/snapshots_type.go +++ b/pkg/coredata/snapshots_type.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -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(): diff --git a/pkg/coredata/snapshottable.go b/pkg/coredata/snapshottable.go index 05d4a759c..9f62123b3 100644 --- a/pkg/coredata/snapshottable.go +++ b/pkg/coredata/snapshottable.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -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: