Add finding and obligation publish to document system
Replace the old snapshot-based approach with the new publish document system for findings and obligations. Includes GraphQL mutations, MCP tools, CLI commands, e2e tests, frontend publish dialogs, and snapshot-to-document migration tools. Remove snapshot mode entirely from findings and obligations: drop snapshotId from GraphQL schemas, filters, resolvers, MCP spec, frontend routes, pages, and helpers. The snapshot_id column remains in the database but is now filtered out with snapshot_id IS NULL. Remove auditor's ability to publish SoA. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -120,6 +120,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND id = @finding_id
|
||||
AND snapshot_id IS NULL
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
@@ -158,6 +159,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
AND %s
|
||||
`
|
||||
|
||||
@@ -212,6 +214,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
@@ -412,95 +415,6 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs Findings) Snapshot(ctx context.Context, conn pg.Tx, 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.Querier,
|
||||
@@ -538,6 +452,7 @@ WITH f AS (
|
||||
findings_audits fa ON fi.id = fa.finding_id
|
||||
WHERE
|
||||
fa.audit_id = @audit_id
|
||||
AND fi.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
@@ -605,6 +520,7 @@ WITH f AS (
|
||||
findings_audits fa ON fi.id = fa.finding_id
|
||||
WHERE
|
||||
fa.audit_id = @audit_id
|
||||
AND fi.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
@@ -631,3 +547,167 @@ WHERE
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (fs *Findings) LoadAllByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID 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 organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
ORDER BY
|
||||
reference_id ASC
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.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) GetGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
organizationID gid.GID,
|
||||
) (*gid.GID, error) {
|
||||
var documentID *gid.GID
|
||||
|
||||
err := conn.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
findings_document_id
|
||||
FROM
|
||||
generated_documents
|
||||
WHERE
|
||||
organization_id = @organization_id
|
||||
`,
|
||||
pgx.NamedArgs{"organization_id": organizationID},
|
||||
).Scan(&documentID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get finding list document ID: %w", err)
|
||||
}
|
||||
|
||||
return documentID, nil
|
||||
}
|
||||
|
||||
func (f Finding) UpsertGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
organizationID gid.GID,
|
||||
tenantID gid.TenantID,
|
||||
documentID gid.GID,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO generated_documents (
|
||||
organization_id,
|
||||
tenant_id,
|
||||
findings_document_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@findings_document_id,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
ON CONFLICT (organization_id) DO UPDATE
|
||||
SET
|
||||
findings_document_id = @findings_document_id,
|
||||
updated_at = @updated_at
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"organization_id": organizationID,
|
||||
"tenant_id": tenantID,
|
||||
"findings_document_id": documentID,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot upsert finding list document ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Finding) ClearGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
documentIDs []gid.GID,
|
||||
) error {
|
||||
ids := make([]string, len(documentIDs))
|
||||
for i, id := range documentIDs {
|
||||
ids[i] = id.String()
|
||||
}
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
UPDATE
|
||||
generated_documents
|
||||
SET
|
||||
findings_document_id = NULL,
|
||||
updated_at = @now
|
||||
WHERE
|
||||
findings_document_id = ANY(@ids)
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"ids": ids,
|
||||
"now": time.Now(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot clear finding list document references: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,34 +21,29 @@ import (
|
||||
|
||||
type (
|
||||
FindingFilter struct {
|
||||
snapshotID **gid.GID
|
||||
kind *FindingKind
|
||||
status *FindingStatus
|
||||
priority *FindingPriority
|
||||
ownerID *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,
|
||||
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,
|
||||
@@ -59,13 +54,6 @@ func (f *FindingFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
"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)
|
||||
@@ -92,15 +80,6 @@ func (f *FindingFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
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
|
||||
|
||||
17
pkg/coredata/migrations/20260422T130000Z.sql
Normal file
17
pkg/coredata/migrations/20260422T130000Z.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- 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.
|
||||
|
||||
ALTER TABLE generated_documents
|
||||
ADD COLUMN findings_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
|
||||
ADD COLUMN obligations_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL;
|
||||
@@ -108,6 +108,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND id = @obligation_id
|
||||
AND snapshot_id IS NULL
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
@@ -136,7 +137,6 @@ func (os *Obligations) CountByOrganizationID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -146,14 +146,13 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND snapshot_id IS NULL
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
@@ -171,7 +170,6 @@ func (os *Obligations) CountByRiskID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
riskID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -186,20 +184,19 @@ WITH obls AS (
|
||||
risks_obligations ro ON o.id = ro.obligation_id
|
||||
WHERE
|
||||
ro.risk_id = @risk_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"risk_id": riskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
@@ -218,7 +215,6 @@ func (os *Obligations) LoadByOrganizationID(
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -243,15 +239,14 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND snapshot_id IS NULL
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.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)
|
||||
@@ -275,7 +270,6 @@ func (os *Obligations) LoadByRiskID(
|
||||
scope Scoper,
|
||||
riskID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -304,6 +298,7 @@ WITH obls AS (
|
||||
risks_obligations ro ON o.id = ro.obligation_id
|
||||
WHERE
|
||||
ro.risk_id = @risk_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
@@ -326,14 +321,12 @@ FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"risk_id": riskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -356,7 +349,6 @@ func (os *Obligations) CountByControlID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -370,20 +362,19 @@ WITH obls AS (
|
||||
controls_obligations co ON o.id = co.obligation_id
|
||||
WHERE
|
||||
co.control_id = @control_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
@@ -402,7 +393,6 @@ func (os *Obligations) LoadByControlID(
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -430,6 +420,7 @@ WITH obls AS (
|
||||
controls_obligations co ON o.id = co.obligation_id
|
||||
WHERE
|
||||
co.control_id = @control_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
@@ -452,13 +443,11 @@ FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -625,13 +614,15 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (os Obligations) Snapshot(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error {
|
||||
query := `
|
||||
INSERT INTO obligations (
|
||||
func (os *Obligations) LoadAllByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
organization_id,
|
||||
area,
|
||||
source,
|
||||
@@ -643,44 +634,142 @@ INSERT INTO obligations (
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(@tenant_id), @obligation_entity_type),
|
||||
@tenant_id,
|
||||
@snapshot_id,
|
||||
o.id,
|
||||
o.organization_id,
|
||||
o.area,
|
||||
o.source,
|
||||
o.requirement,
|
||||
o.actions_to_be_implemented,
|
||||
o.regulator,
|
||||
o.owner_profile_id,
|
||||
o.last_review_date,
|
||||
o.due_date,
|
||||
o.status,
|
||||
o.type,
|
||||
o.created_at,
|
||||
o.updated_at
|
||||
FROM obligations o
|
||||
WHERE %s AND o.organization_id = @organization_id AND o.snapshot_id IS NULL
|
||||
`
|
||||
FROM
|
||||
obligations
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
ORDER BY
|
||||
created_at ASC
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
"obligation_entity_type": ObligationEntityType,
|
||||
}
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, query, args)
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert obligation snapshots: %w", err)
|
||||
return fmt.Errorf("cannot query obligations: %w", err)
|
||||
}
|
||||
|
||||
obligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Obligation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect obligations: %w", err)
|
||||
}
|
||||
|
||||
*os = obligations
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Obligation) GetGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
organizationID gid.GID,
|
||||
) (*gid.GID, error) {
|
||||
var documentID *gid.GID
|
||||
|
||||
err := conn.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
obligations_document_id
|
||||
FROM
|
||||
generated_documents
|
||||
WHERE
|
||||
organization_id = @organization_id
|
||||
`,
|
||||
pgx.NamedArgs{"organization_id": organizationID},
|
||||
).Scan(&documentID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get obligation list document ID: %w", err)
|
||||
}
|
||||
|
||||
return documentID, nil
|
||||
}
|
||||
|
||||
func (o Obligation) UpsertGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
organizationID gid.GID,
|
||||
tenantID gid.TenantID,
|
||||
documentID gid.GID,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO generated_documents (
|
||||
organization_id,
|
||||
tenant_id,
|
||||
obligations_document_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@obligations_document_id,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
ON CONFLICT (organization_id) DO UPDATE
|
||||
SET
|
||||
obligations_document_id = @obligations_document_id,
|
||||
updated_at = @updated_at
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"organization_id": organizationID,
|
||||
"tenant_id": tenantID,
|
||||
"obligations_document_id": documentID,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot upsert obligation list document ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Obligation) ClearGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
documentIDs []gid.GID,
|
||||
) error {
|
||||
ids := make([]string, len(documentIDs))
|
||||
for i, id := range documentIDs {
|
||||
ids[i] = id.String()
|
||||
}
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
UPDATE
|
||||
generated_documents
|
||||
SET
|
||||
obligations_document_id = NULL,
|
||||
updated_at = @now
|
||||
WHERE
|
||||
obligations_document_id = ANY(@ids)
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"ids": ids,
|
||||
"now": time.Now(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot clear obligation list document references: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// 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
|
||||
// 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 (
|
||||
ObligationFilter struct {
|
||||
snapshotID **gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewObligationFilter(snapshotID **gid.GID) *ObligationFilter {
|
||||
return &ObligationFilter{
|
||||
snapshotID: snapshotID,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ObligationFilter) SQLArguments() pgx.NamedArgs {
|
||||
args := pgx.NamedArgs{}
|
||||
|
||||
if f.snapshotID != nil && *f.snapshotID != nil {
|
||||
args["filter_snapshot_id"] = **f.snapshotID
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (f *ObligationFilter) SQLFragment() string {
|
||||
if f.snapshotID == nil {
|
||||
return "TRUE"
|
||||
}
|
||||
|
||||
if *f.snapshotID == nil {
|
||||
return "snapshot_id IS NULL"
|
||||
} else {
|
||||
return "snapshot_id = @filter_snapshot_id"
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,6 @@ func SnapshotsTypes() []SnapshotsType {
|
||||
return []SnapshotsType{
|
||||
SnapshotsTypeRisks,
|
||||
SnapshotsTypeVendors,
|
||||
SnapshotsTypeFindings,
|
||||
SnapshotsTypeObligations,
|
||||
SnapshotsTypeProcessingActivities,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
|
||||
switch snapshotType {
|
||||
case SnapshotsTypeRisks:
|
||||
return Risks{}, nil
|
||||
case SnapshotsTypeFindings:
|
||||
return Findings{}, nil
|
||||
case SnapshotsTypeObligations:
|
||||
return Obligations{}, nil
|
||||
case SnapshotsTypeProcessingActivities:
|
||||
return ProcessingActivities{}, nil
|
||||
case SnapshotsTypeVendors:
|
||||
|
||||
Reference in New Issue
Block a user