From 3ff66757add1818502b5f69d464c613e99875b87 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 6 May 2026 17:24:58 +0200 Subject: [PATCH] Remove deprecated snapshot system The register/document model has fully replaced the snapshot system. Delete every snapshot-scoped row and strip the application code that referenced them: SnapshotID/SourceID struct fields, snapshot_id IS NULL filters, snapshot columns from SELECT/INSERT statements and named args, and the eight migrate-*-snapshots-to-documents one-shot tools. The remaining snapshot_id / source_id columns, the snapshots and controls_snapshots tables, the snapshots_type enum, and the snapshot-scoped indexes are now unused; they are dropped in a follow-up schema migration so this change can roll back cleanly without leaving orphaned data. Signed-off-by: Sacha Al Himdani --- .../main.go | 439 ---------- .../main.go | 438 ---------- .../main.go | 492 ----------- .../main.go | 455 ----------- .../main.go | 763 ------------------ .../main.go | 530 ------------ .../main.go | 539 ------------- .../main.go | 754 ----------------- contrib/claude/coredata.md | 1 - pkg/coredata/applicability_statement.go | 13 - pkg/coredata/asset.go | 7 - .../data_protection_impact_assessment.go | 13 - pkg/coredata/datum.go | 7 - pkg/coredata/finding.go | 23 +- pkg/coredata/migrations/20260506T150405Z.sql | 30 + pkg/coredata/obligation.go | 36 +- pkg/coredata/processing_activities.go | 21 - .../processing_activity_third_party.go | 1 - pkg/coredata/risk.go | 9 +- pkg/coredata/statement_of_applicability.go | 5 - pkg/coredata/third_party.go | 5 - ...hird_party_business_associate_agreement.go | 12 - pkg/coredata/third_party_compliance_report.go | 3 - pkg/coredata/third_party_contact.go | 4 - .../third_party_data_privacy_agreement.go | 11 - pkg/coredata/third_party_risk_assessment.go | 3 - pkg/coredata/third_party_service.go | 4 - pkg/coredata/transfer_impact_assessment.go | 13 - 28 files changed, 34 insertions(+), 4597 deletions(-) delete mode 100644 cmd/migrate-asset-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-data-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-finding-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-obligation-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-processing-activity-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-risk-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-soa-snapshots-to-documents/main.go delete mode 100644 cmd/migrate-third-party-snapshots-to-documents/main.go create mode 100644 pkg/coredata/migrations/20260506T150405Z.sql diff --git a/cmd/migrate-asset-snapshots-to-documents/main.go b/cmd/migrate-asset-snapshots-to-documents/main.go deleted file mode 100644 index 902eaf8e9..000000000 --- a/cmd/migrate-asset-snapshots-to-documents/main.go +++ /dev/null @@ -1,439 +0,0 @@ -// 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. - -// Command migrate-asset-snapshots-to-documents creates documents and document -// versions from existing asset snapshots. For each organization that has asset -// snapshots, it generates an asset list document using the same ProseMirror -// builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "strings" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrate(ctx, tx, dryRun) - }) -} - -type orgWithAssetSnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type assetSnapshot struct { - snapshotID string - publishedAt time.Time -} - -func migrate(ctx context.Context, tx pg.Tx, dryRun bool) error { - orgs, err := loadOrgsWithAssetSnapshots(ctx, tx) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with asset snapshots to migrate") - return nil - } - - var stats struct { - documents, versions int - } - - for _, org := range orgs { - snapshots, err := loadAssetSnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if dryRun { - fmt.Printf("would migrate org %s (%s) — %d asset snapshot(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - continue - } - - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document for org %s: %w", org.organizationID, err) - } - stats.documents++ - - _, err = tx.Exec( - ctx, - `INSERT INTO generated_documents (organization_id, tenant_id, asset_list_document_id, created_at, updated_at) -VALUES (@organization_id, @tenant_id, @asset_list_document_id, @created_at, @updated_at) -ON CONFLICT (organization_id) DO UPDATE SET asset_list_document_id = @asset_list_document_id, updated_at = @updated_at`, - pgx.NamedArgs{ - "organization_id": org.organizationID, - "tenant_id": org.tenantID, - "asset_list_document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document to org %s: %w", org.organizationID, err) - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s of org %s: %w", - snap.snapshotID, org.organizationID, err) - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'PORTRAIT'::document_version_orientation, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": "Assets", - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - stats.versions++ - } - - fmt.Printf("migrated org %s (%s) — %d version(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\ncreated %d document(s), %d version(s)\n", stats.documents, stats.versions) - - return nil -} - -func loadOrgsWithAssetSnapshots(ctx context.Context, tx pg.Tx) ([]orgWithAssetSnapshots, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id AND gd.asset_list_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM assets a - WHERE a.organization_id = o.id AND a.snapshot_id IS NOT NULL - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with asset snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithAssetSnapshots - for rows.Next() { - var o orgWithAssetSnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadAssetSnapshots(ctx context.Context, tx pg.Tx, organizationID gid.GID) ([]assetSnapshot, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'ASSETS' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query asset snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []assetSnapshot - for rows.Next() { - var s assetSnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -func buildSnapshotContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - a.id, - a.name, - a.asset_type, - a.amount, - a.data_types_stored, - COALESCE(p.full_name, '-') -FROM assets a -LEFT JOIN iam_membership_profiles p ON p.id = a.owner_profile_id -WHERE a.snapshot_id = @snapshot_id -ORDER BY a.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot assets: %w", err) - } - defer rows.Close() - - type assetInfo struct { - id string - name string - assetType string - amount int - dataTypesStored string - ownerName string - } - - var assets []assetInfo - for rows.Next() { - var a assetInfo - if err := rows.Scan(&a.id, &a.name, &a.assetType, &a.amount, &a.dataTypesStored, &a.ownerName); err != nil { - return "", fmt.Errorf("cannot scan asset: %w", err) - } - assets = append(assets, a) - } - if err := rows.Err(); err != nil { - return "", err - } - - thirdPartyRows, err := tx.Query( - ctx, - ` -SELECT - av.asset_id, - v.name -FROM asset_third_parties av -JOIN third_parties v ON v.id = av.third_party_id -WHERE av.snapshot_id = @snapshot_id -ORDER BY v.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot asset thirdParties: %w", err) - } - defer thirdPartyRows.Close() - - thirdPartiesByAsset := make(map[string][]string) - for thirdPartyRows.Next() { - var assetID, thirdPartyName string - if err := thirdPartyRows.Scan(&assetID, &thirdPartyName); err != nil { - return "", fmt.Errorf("cannot scan thirdParty: %w", err) - } - thirdPartiesByAsset[assetID] = append(thirdPartiesByAsset[assetID], thirdPartyName) - } - if err := thirdPartyRows.Err(); err != nil { - return "", err - } - - assetRows := make([]docgen.AssetListRow, len(assets)) - for i, a := range assets { - thirdParties := "-" - if v, ok := thirdPartiesByAsset[a.id]; ok && len(v) > 0 { - thirdParties = strings.Join(v, ", ") - } - - assetRows[i] = docgen.AssetListRow{ - Name: a.name, - AssetType: formatAssetTypeString(a.assetType), - Amount: a.amount, - DataTypesStored: a.dataTypesStored, - Owner: a.ownerName, - ThirdParties: thirdParties, - } - } - - docData := docgen.AssetListData{ - Title: "Assets", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalAssets: len(assetRows), - Rows: assetRows, - } - - return probo.BuildAssetListDocument(docData) -} - -func formatAssetTypeString(t string) string { - switch t { - case "PHYSICAL": - return "Physical" - case "VIRTUAL": - return "Virtual" - default: - return t - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN") - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-data-snapshots-to-documents/main.go b/cmd/migrate-data-snapshots-to-documents/main.go deleted file mode 100644 index ca5dfd375..000000000 --- a/cmd/migrate-data-snapshots-to-documents/main.go +++ /dev/null @@ -1,438 +0,0 @@ -// 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. - -// Command migrate-data-snapshots-to-documents creates documents and document -// versions from existing data snapshots. For each organization that has data -// snapshots, it generates a data list document using the same ProseMirror -// builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "strings" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrate(ctx, tx, dryRun) - }) -} - -type orgWithDataSnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type dataSnapshot struct { - snapshotID string - publishedAt time.Time -} - -func migrate(ctx context.Context, tx pg.Tx, dryRun bool) error { - orgs, err := loadOrgsWithDataSnapshots(ctx, tx) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with data snapshots to migrate") - return nil - } - - var stats struct { - documents, versions int - } - - for _, org := range orgs { - snapshots, err := loadDataSnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if dryRun { - fmt.Printf("would migrate org %s (%s) — %d data snapshot(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - continue - } - - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document for org %s: %w", org.organizationID, err) - } - stats.documents++ - - _, err = tx.Exec( - ctx, - `INSERT INTO generated_documents (organization_id, tenant_id, data_document_id, created_at, updated_at) -VALUES (@organization_id, @tenant_id, @data_document_id, @created_at, @updated_at) -ON CONFLICT (organization_id) DO UPDATE SET data_document_id = @data_document_id, updated_at = @updated_at`, - pgx.NamedArgs{ - "organization_id": org.organizationID, - "tenant_id": org.tenantID, - "data_document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document to org %s: %w", org.organizationID, err) - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s of org %s: %w", - snap.snapshotID, org.organizationID, err) - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'PORTRAIT'::document_version_orientation, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": "Data", - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - stats.versions++ - } - - fmt.Printf("migrated org %s (%s) — %d version(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\ncreated %d document(s), %d version(s)\n", stats.documents, stats.versions) - - return nil -} - -func loadOrgsWithDataSnapshots(ctx context.Context, tx pg.Tx) ([]orgWithDataSnapshots, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id AND gd.data_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM data d - WHERE d.organization_id = o.id AND d.snapshot_id IS NOT NULL - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with data snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithDataSnapshots - for rows.Next() { - var o orgWithDataSnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadDataSnapshots(ctx context.Context, tx pg.Tx, organizationID gid.GID) ([]dataSnapshot, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'DATA' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query data snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []dataSnapshot - for rows.Next() { - var s dataSnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -func buildSnapshotContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - d.id, - d.name, - d.data_classification, - COALESCE(p.full_name, '-') -FROM data d -LEFT JOIN iam_membership_profiles p ON p.id = d.owner_profile_id -WHERE d.snapshot_id = @snapshot_id -ORDER BY d.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot data: %w", err) - } - defer rows.Close() - - type datumInfo struct { - id string - name string - classification string - ownerName string - } - - var data []datumInfo - for rows.Next() { - var d datumInfo - if err := rows.Scan(&d.id, &d.name, &d.classification, &d.ownerName); err != nil { - return "", fmt.Errorf("cannot scan datum: %w", err) - } - data = append(data, d) - } - if err := rows.Err(); err != nil { - return "", err - } - - // Load thirdParties for each datum in this snapshot. - thirdPartyRows, err := tx.Query( - ctx, - ` -SELECT - dv.datum_id, - v.name -FROM data_third_parties dv -JOIN third_parties v ON v.id = dv.third_party_id -WHERE dv.snapshot_id = @snapshot_id -ORDER BY v.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot data thirdParties: %w", err) - } - defer thirdPartyRows.Close() - - thirdPartiesByDatum := make(map[string][]string) - for thirdPartyRows.Next() { - var datumID, thirdPartyName string - if err := thirdPartyRows.Scan(&datumID, &thirdPartyName); err != nil { - return "", fmt.Errorf("cannot scan thirdParty: %w", err) - } - thirdPartiesByDatum[datumID] = append(thirdPartiesByDatum[datumID], thirdPartyName) - } - if err := thirdPartyRows.Err(); err != nil { - return "", err - } - - dataRows := make([]docgen.DataListRow, len(data)) - for i, d := range data { - thirdParties := "-" - if v, ok := thirdPartiesByDatum[d.id]; ok && len(v) > 0 { - thirdParties = strings.Join(v, ", ") - } - - dataRows[i] = docgen.DataListRow{ - Name: d.name, - Classification: formatClassificationString(d.classification), - Owner: d.ownerName, - ThirdParties: thirdParties, - } - } - - docData := docgen.DataListData{ - Title: "Data", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalData: len(dataRows), - Rows: dataRows, - } - - return probo.BuildDataListDocument(docData) -} - -func formatClassificationString(c string) string { - switch c { - case "PUBLIC": - return "Public" - case "INTERNAL": - return "Internal" - case "CONFIDENTIAL": - return "Confidential" - case "SECRET": - return "Secret" - default: - return c - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN") - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-finding-snapshots-to-documents/main.go b/cmd/migrate-finding-snapshots-to-documents/main.go deleted file mode 100644 index 2dcc75430..000000000 --- a/cmd/migrate-finding-snapshots-to-documents/main.go +++ /dev/null @@ -1,492 +0,0 @@ -// 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. - -// Command migrate-finding-snapshots-to-documents creates documents and document -// versions from existing finding snapshots. For each organization that has finding -// snapshots, it generates a finding register document using the same ProseMirror -// builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrate(ctx, tx, dryRun) - }) -} - -type orgWithFindingSnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type findingSnapshot struct { - snapshotID string - publishedAt time.Time -} - -func migrate(ctx context.Context, tx pg.Tx, dryRun bool) error { - orgs, err := loadOrgsWithFindingSnapshots(ctx, tx) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with finding snapshots to migrate") - return nil - } - - var stats struct { - documents, versions int - } - - for _, org := range orgs { - snapshots, err := loadFindingSnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if dryRun { - fmt.Printf("would migrate org %s (%s) — %d finding snapshot(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - continue - } - - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document for org %s: %w", org.organizationID, err) - } - stats.documents++ - - _, err = tx.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": org.organizationID, - "tenant_id": org.tenantID, - "findings_document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document to org %s: %w", org.organizationID, err) - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s of org %s: %w", - snap.snapshotID, org.organizationID, err) - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'LANDSCAPE'::document_version_orientation, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": "Findings", - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - stats.versions++ - } - - fmt.Printf("migrated org %s (%s) — %d version(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\ncreated %d document(s), %d version(s)\n", stats.documents, stats.versions) - - return nil -} - -func loadOrgsWithFindingSnapshots(ctx context.Context, tx pg.Tx) ([]orgWithFindingSnapshots, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id AND gd.findings_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM findings f - WHERE f.organization_id = o.id AND f.snapshot_id IS NOT NULL - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with finding snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithFindingSnapshots - for rows.Next() { - var o orgWithFindingSnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadFindingSnapshots(ctx context.Context, tx pg.Tx, organizationID gid.GID) ([]findingSnapshot, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'FINDINGS' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query finding snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []findingSnapshot - for rows.Next() { - var s findingSnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -func buildSnapshotContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - f.reference_id, - f.kind, - f.description, - f.source, - f.identified_on, - f.root_cause, - f.corrective_action, - f.effectiveness_check, - f.status, - f.priority, - f.due_date, - COALESCE(p.full_name, '-') -FROM findings f -LEFT JOIN iam_membership_profiles p ON p.id = f.owner_id -WHERE f.snapshot_id = @snapshot_id -ORDER BY f.reference_id ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot findings: %w", err) - } - defer rows.Close() - - type findingInfo struct { - referenceID string - kind string - description *string - source *string - identifiedOn *time.Time - rootCause *string - correctiveAction *string - effectivenessCheck *string - status string - priority string - dueDate *time.Time - ownerName string - } - - var findings []findingInfo - for rows.Next() { - var f findingInfo - if err := rows.Scan(&f.referenceID, &f.kind, &f.description, &f.source, &f.identifiedOn, &f.rootCause, &f.correctiveAction, &f.effectivenessCheck, &f.status, &f.priority, &f.dueDate, &f.ownerName); err != nil { - return "", fmt.Errorf("cannot scan finding: %w", err) - } - findings = append(findings, f) - } - if err := rows.Err(); err != nil { - return "", err - } - - findingRows := make([]docgen.FindingListRow, len(findings)) - for i, f := range findings { - description := "-" - if f.description != nil && *f.description != "" { - description = *f.description - } - - source := "-" - if f.source != nil && *f.source != "" { - source = *f.source - } - - identifiedOn := "-" - if f.identifiedOn != nil { - identifiedOn = f.identifiedOn.Format("2006-01-02") - } - - rootCause := "-" - if f.rootCause != nil && *f.rootCause != "" { - rootCause = *f.rootCause - } - - correctiveAction := "-" - if f.correctiveAction != nil && *f.correctiveAction != "" { - correctiveAction = *f.correctiveAction - } - - effectivenessCheck := "-" - if f.effectivenessCheck != nil && *f.effectivenessCheck != "" { - effectivenessCheck = *f.effectivenessCheck - } - - dueDate := "-" - if f.dueDate != nil { - dueDate = f.dueDate.Format("2006-01-02") - } - - findingRows[i] = docgen.FindingListRow{ - ReferenceID: f.referenceID, - Kind: formatFindingKindString(f.kind), - Description: description, - Source: source, - IdentifiedOn: identifiedOn, - RootCause: rootCause, - CorrectiveAction: correctiveAction, - EffectivenessCheck: effectivenessCheck, - Status: formatFindingStatusString(f.status), - Priority: formatFindingPriorityString(f.priority), - Owner: f.ownerName, - DueDate: dueDate, - } - } - - docData := docgen.FindingListData{ - Title: "Findings", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalFindings: len(findingRows), - Rows: findingRows, - } - - return probo.BuildFindingListDocument(docData) -} - -func formatFindingKindString(k string) string { - switch k { - case "MINOR_NONCONFORMITY": - return "Minor Nonconformity" - case "MAJOR_NONCONFORMITY": - return "Major Nonconformity" - case "OBSERVATION": - return "Observation" - case "EXCEPTION": - return "Exception" - default: - return k - } -} - -func formatFindingStatusString(s string) string { - switch s { - case "OPEN": - return "Open" - case "IN_PROGRESS": - return "In Progress" - case "CLOSED": - return "Closed" - case "RISK_ACCEPTED": - return "Risk Accepted" - case "MITIGATED": - return "Mitigated" - case "FALSE_POSITIVE": - return "False Positive" - default: - return s - } -} - -func formatFindingPriorityString(p string) string { - switch p { - case "LOW": - return "Low" - case "MEDIUM": - return "Medium" - case "HIGH": - return "High" - default: - return p - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN: %w", err) - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-obligation-snapshots-to-documents/main.go b/cmd/migrate-obligation-snapshots-to-documents/main.go deleted file mode 100644 index b2fae5e2a..000000000 --- a/cmd/migrate-obligation-snapshots-to-documents/main.go +++ /dev/null @@ -1,455 +0,0 @@ -// 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. - -// Command migrate-obligation-snapshots-to-documents creates documents and document -// versions from existing obligation snapshots. For each organization that has -// obligation snapshots, it generates an obligation register document using the same -// ProseMirror builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrate(ctx, tx, dryRun) - }) -} - -type orgWithObligationSnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type obligationSnapshot struct { - snapshotID string - publishedAt time.Time -} - -func migrate(ctx context.Context, tx pg.Tx, dryRun bool) error { - orgs, err := loadOrgsWithObligationSnapshots(ctx, tx) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with obligation snapshots to migrate") - return nil - } - - var stats struct { - documents, versions int - } - - for _, org := range orgs { - snapshots, err := loadObligationSnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if dryRun { - fmt.Printf("would migrate org %s (%s) — %d obligation snapshot(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - continue - } - - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document for org %s: %w", org.organizationID, err) - } - stats.documents++ - - _, err = tx.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": org.organizationID, - "tenant_id": org.tenantID, - "obligations_document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document to org %s: %w", org.organizationID, err) - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s of org %s: %w", - snap.snapshotID, org.organizationID, err) - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'LANDSCAPE'::document_version_orientation, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": "Obligations", - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - stats.versions++ - } - - fmt.Printf("migrated org %s (%s) — %d version(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\ncreated %d document(s), %d version(s)\n", stats.documents, stats.versions) - - return nil -} - -func loadOrgsWithObligationSnapshots(ctx context.Context, tx pg.Tx) ([]orgWithObligationSnapshots, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id AND gd.obligations_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM obligations ob - WHERE ob.organization_id = o.id AND ob.snapshot_id IS NOT NULL - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with obligation snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithObligationSnapshots - for rows.Next() { - var o orgWithObligationSnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadObligationSnapshots(ctx context.Context, tx pg.Tx, organizationID gid.GID) ([]obligationSnapshot, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'OBLIGATIONS' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query obligation snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []obligationSnapshot - for rows.Next() { - var s obligationSnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -func buildSnapshotContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - ob.area, - ob.source, - ob.requirement, - ob.actions_to_be_implemented, - ob.status, - ob.type, - ob.regulator, - ob.due_date, - COALESCE(p.full_name, '-') -FROM obligations ob -LEFT JOIN iam_membership_profiles p ON p.id = ob.owner_profile_id -WHERE ob.snapshot_id = @snapshot_id -ORDER BY ob.created_at ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot obligations: %w", err) - } - defer rows.Close() - - type obligationInfo struct { - area *string - source *string - requirement *string - actionsToBeImplemented *string - status string - oblType string - regulator *string - dueDate *time.Time - ownerName string - } - - var obligations []obligationInfo - for rows.Next() { - var o obligationInfo - if err := rows.Scan(&o.area, &o.source, &o.requirement, &o.actionsToBeImplemented, &o.status, &o.oblType, &o.regulator, &o.dueDate, &o.ownerName); err != nil { - return "", fmt.Errorf("cannot scan obligation: %w", err) - } - obligations = append(obligations, o) - } - if err := rows.Err(); err != nil { - return "", err - } - - obligationRows := make([]docgen.ObligationListRow, len(obligations)) - for i, o := range obligations { - area := "-" - if o.area != nil && *o.area != "" { - area = *o.area - } - - source := "-" - if o.source != nil && *o.source != "" { - source = *o.source - } - - requirement := "-" - if o.requirement != nil && *o.requirement != "" { - requirement = *o.requirement - } - - actionsToBeImplemented := "-" - if o.actionsToBeImplemented != nil && *o.actionsToBeImplemented != "" { - actionsToBeImplemented = *o.actionsToBeImplemented - } - - regulator := "-" - if o.regulator != nil && *o.regulator != "" { - regulator = *o.regulator - } - - dueDate := "-" - if o.dueDate != nil { - dueDate = o.dueDate.Format("2006-01-02") - } - - obligationRows[i] = docgen.ObligationListRow{ - Area: area, - Source: source, - Requirement: requirement, - ActionsToBeImplemented: actionsToBeImplemented, - Status: formatObligationStatusString(o.status), - Type: formatObligationTypeString(o.oblType), - Regulator: regulator, - Owner: o.ownerName, - DueDate: dueDate, - } - } - - docData := docgen.ObligationListData{ - Title: "Obligations", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalObligations: len(obligationRows), - Rows: obligationRows, - } - - return probo.BuildObligationListDocument(docData) -} - -func formatObligationStatusString(s string) string { - switch s { - case "NON_COMPLIANT": - return "Non Compliant" - case "PARTIALLY_COMPLIANT": - return "Partially Compliant" - case "COMPLIANT": - return "Compliant" - default: - return s - } -} - -func formatObligationTypeString(t string) string { - switch t { - case "LEGAL": - return "Legal" - case "CONTRACTUAL": - return "Contractual" - default: - return t - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN: %w", err) - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-processing-activity-snapshots-to-documents/main.go b/cmd/migrate-processing-activity-snapshots-to-documents/main.go deleted file mode 100644 index 1666a5e13..000000000 --- a/cmd/migrate-processing-activity-snapshots-to-documents/main.go +++ /dev/null @@ -1,763 +0,0 @@ -// 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. - -// Command migrate-processing-activity-snapshots-to-documents creates documents -// and document versions from existing processing activity snapshots. For each -// organization that has PROCESSING_ACTIVITIES snapshots, it produces three -// register documents — Processing Activities, Data Protection Impact -// Assessments, and Transfer Impact Assessments — using the same ProseMirror -// builders as the publish flow, with one version per snapshot ordered by date. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "strings" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrate(ctx, tx, dryRun) - }) -} - -type orgWithSnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type processingActivitySnapshot struct { - snapshotID string - publishedAt time.Time -} - -type kind struct { - name string - title string - column string - buildFn func(ctx context.Context, tx pg.Tx, snapshotID string, orgName string, publishedAt time.Time) (string, int, error) -} - -func migrate(ctx context.Context, tx pg.Tx, dryRun bool) error { - orgs, err := loadOrgsWithSnapshots(ctx, tx) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with processing activity snapshots to migrate") - return nil - } - - kinds := []kind{ - {name: "processing-activity", title: "Processing Activities", column: "processing_activities_document_id", buildFn: buildProcessingActivityContent}, - {name: "dpia", title: "Data Protection Impact Assessments", column: "data_protection_impact_assessments_document_id", buildFn: buildDPIAContent}, - {name: "tia", title: "Transfer Impact Assessments", column: "transfer_impact_assessments_document_id", buildFn: buildTIAContent}, - } - - var stats struct { - documents, versions int - } - - for _, org := range orgs { - snapshots, err := loadSnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if dryRun { - fmt.Printf("would migrate org %s (%s) — %d snapshot(s) × %d kind(s)\n", - org.organizationID, org.organizationName, len(snapshots), len(kinds)) - continue - } - - for _, k := range kinds { - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - versionsInserted := 0 - for major, snap := range snapshots { - content, count, err := k.buildFn(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build %s content for snapshot %s of org %s: %w", - k.name, snap.snapshotID, org.organizationID, err) - } - if count == 0 { - continue - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'PORTRAIT'::document_version_orientation, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": k.title, - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert %s version for snapshot %s: %w", k.name, snap.snapshotID, err) - } - versionsInserted++ - } - - if versionsInserted == 0 { - continue - } - - _, err := tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": versionsInserted, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert %s document for org %s: %w", k.name, org.organizationID, err) - } - stats.documents++ - stats.versions += versionsInserted - - _, err = tx.Exec( - ctx, - fmt.Sprintf(` -INSERT INTO generated_documents (organization_id, tenant_id, %s, created_at, updated_at) -VALUES (@organization_id, @tenant_id, @document_id, @created_at, @updated_at) -ON CONFLICT (organization_id) DO UPDATE SET %s = @document_id, updated_at = @updated_at`, k.column, k.column), - pgx.NamedArgs{ - "organization_id": org.organizationID, - "tenant_id": org.tenantID, - "document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link %s document to org %s: %w", k.name, org.organizationID, err) - } - } - - fmt.Printf("migrated org %s (%s) — %d snapshot(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\ncreated %d document(s), %d version(s)\n", stats.documents, stats.versions) - return nil -} - -func loadOrgsWithSnapshots(ctx context.Context, tx pg.Tx) ([]orgWithSnapshots, error) { - rows, err := tx.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id - AND gd.processing_activities_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM snapshots s - WHERE s.organization_id = o.id AND s.type = 'PROCESSING_ACTIVITIES' - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithSnapshots - for rows.Next() { - var o orgWithSnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - return result, rows.Err() -} - -func loadSnapshots(ctx context.Context, tx pg.Tx, organizationID gid.GID) ([]processingActivitySnapshot, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'PROCESSING_ACTIVITIES' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []processingActivitySnapshot - for rows.Next() { - var s processingActivitySnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - return result, rows.Err() -} - -func buildProcessingActivityContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, int, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - pa.id, - pa.name, - pa.purpose, - pa.data_subject_category, - pa.personal_data_category, - pa.special_or_criminal_data, - pa.consent_evidence_link, - pa.lawful_basis, - pa.recipients, - pa.location, - pa.international_transfers, - pa.transfer_safeguards, - pa.retention_period, - pa.security_measures, - pa.data_protection_impact_assessment_needed, - pa.transfer_impact_assessment_needed, - pa.last_review_date, - pa.next_review_date, - pa.role, - COALESCE(p.full_name, '') -FROM processing_activities pa -LEFT JOIN iam_membership_profiles p ON p.id = pa.dpo_profile_id -WHERE pa.snapshot_id = @snapshot_id -ORDER BY pa.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", 0, fmt.Errorf("cannot load snapshot processing activities: %w", err) - } - defer rows.Close() - - type paRow struct { - id gid.GID - name string - purpose *string - dataSubjectCategory *string - personalDataCategory *string - specialOrCriminalData string - consentEvidenceLink *string - lawfulBasis string - recipients *string - location *string - internationalTransfers bool - transferSafeguards *string - retentionPeriod *string - securityMeasures *string - dataProtectionImpactAssessmentNeeded string - transferImpactAssessmentNeeded string - lastReviewDate *time.Time - nextReviewDate *time.Time - role string - dpoName string - } - - var pas []paRow - for rows.Next() { - var p paRow - if err := rows.Scan( - &p.id, &p.name, &p.purpose, &p.dataSubjectCategory, &p.personalDataCategory, - &p.specialOrCriminalData, &p.consentEvidenceLink, &p.lawfulBasis, - &p.recipients, &p.location, &p.internationalTransfers, &p.transferSafeguards, - &p.retentionPeriod, &p.securityMeasures, - &p.dataProtectionImpactAssessmentNeeded, &p.transferImpactAssessmentNeeded, - &p.lastReviewDate, &p.nextReviewDate, &p.role, &p.dpoName, - ); err != nil { - return "", 0, fmt.Errorf("cannot scan PA: %w", err) - } - pas = append(pas, p) - } - if err := rows.Err(); err != nil { - return "", 0, err - } - - if len(pas) == 0 { - return "", 0, nil - } - - thirdPartyMap, err := loadThirdPartiesForSnapshot(ctx, tx, snapshotID) - if err != nil { - return "", 0, err - } - - listRows := make([]docgen.ProcessingActivityListRow, len(pas)) - for i, p := range pas { - dpo := "Not assigned" - if p.dpoName != "" { - dpo = p.dpoName - } - - thirdParties := "None" - if v, ok := thirdPartyMap[p.id]; ok && len(v) > 0 { - thirdParties = strings.Join(v, ", ") - } - - listRows[i] = docgen.ProcessingActivityListRow{ - Name: p.name, - Purpose: derefOrNotSpecified(p.purpose), - Role: formatRoleString(p.role), - DataSubjectCategory: derefOrNotSpecified(p.dataSubjectCategory), - PersonalDataCategory: derefOrNotSpecified(p.personalDataCategory), - SpecialOrCriminalData: formatSpecialOrCriminalDataString(p.specialOrCriminalData), - LawfulBasis: formatLawfulBasisString(p.lawfulBasis), - ConsentEvidenceLink: derefOrNotSpecified(p.consentEvidenceLink), - Recipients: derefOrNotSpecified(p.recipients), - Location: derefOrNotSpecified(p.location), - InternationalTransfers: yesNoLabel(p.internationalTransfers), - TransferSafeguards: formatTransferSafeguardString(p.transferSafeguards), - RetentionPeriod: derefOrNotSpecified(p.retentionPeriod), - SecurityMeasures: derefOrNotSpecified(p.securityMeasures), - DataProtectionImpactAssessmentNeeded: formatYesNoString(p.dataProtectionImpactAssessmentNeeded), - TransferImpactAssessmentNeeded: formatYesNoString(p.transferImpactAssessmentNeeded), - LastReviewDate: formatDateOrNotSpecified(p.lastReviewDate), - NextReviewDate: formatDateOrNotSpecified(p.nextReviewDate), - DataProtectionOfficer: dpo, - ThirdParties: thirdParties, - } - } - - content, err := probo.BuildProcessingActivityListDocument(docgen.ProcessingActivityListData{ - Title: "Processing Activities", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalProcessingActivities: len(listRows), - Rows: listRows, - }) - if err != nil { - return "", 0, err - } - return content, len(listRows), nil -} - -func loadThirdPartiesForSnapshot(ctx context.Context, tx pg.Tx, snapshotID string) (map[gid.GID][]string, error) { - rows, err := tx.Query( - ctx, - ` -SELECT pav.processing_activity_id, v.name -FROM processing_activity_third_parties pav -INNER JOIN third_parties v ON v.id = pav.third_party_id -WHERE pav.snapshot_id = @snapshot_id -ORDER BY pav.processing_activity_id, v.name; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot thirdParties: %w", err) - } - defer rows.Close() - - result := make(map[gid.GID][]string) - for rows.Next() { - var paID gid.GID - var name string - if err := rows.Scan(&paID, &name); err != nil { - return nil, fmt.Errorf("cannot scan thirdParty row: %w", err) - } - result[paID] = append(result[paID], name) - } - return result, rows.Err() -} - -func buildDPIAContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, int, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - pa.name, - dpia.description, - dpia.necessity_and_proportionality, - dpia.potential_risk, - dpia.mitigations, - dpia.residual_risk -FROM processing_activity_data_protection_impact_assessments dpia -INNER JOIN processing_activities pa ON pa.id = dpia.processing_activity_id -WHERE dpia.snapshot_id = @snapshot_id -ORDER BY pa.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", 0, fmt.Errorf("cannot load snapshot DPIAs: %w", err) - } - defer rows.Close() - - var listRows []docgen.DataProtectionImpactAssessmentListRow - for rows.Next() { - var name string - var description, necessity, potentialRisk, mitigations *string - var residualRisk *string - if err := rows.Scan(&name, &description, &necessity, &potentialRisk, &mitigations, &residualRisk); err != nil { - return "", 0, fmt.Errorf("cannot scan DPIA: %w", err) - } - listRows = append(listRows, docgen.DataProtectionImpactAssessmentListRow{ - ProcessingActivityName: name, - Description: derefOrNotSpecified(description), - NecessityAndProportionality: derefOrNotSpecified(necessity), - PotentialRisk: derefOrNotSpecified(potentialRisk), - Mitigations: derefOrNotSpecified(mitigations), - ResidualRisk: formatResidualRiskString(residualRisk), - }) - } - if err := rows.Err(); err != nil { - return "", 0, err - } - - if len(listRows) == 0 { - return "", 0, nil - } - - content, err := probo.BuildDataProtectionImpactAssessmentListDocument(docgen.DataProtectionImpactAssessmentListData{ - Title: "Data Protection Impact Assessments", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalDataProtectionImpactAssessments: len(listRows), - Rows: listRows, - }) - if err != nil { - return "", 0, err - } - return content, len(listRows), nil -} - -func buildTIAContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, int, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - pa.name, - tia.data_subjects, - tia.legal_mechanism, - tia.transfer, - tia.local_law_risk, - tia.supplementary_measures -FROM processing_activity_transfer_impact_assessments tia -INNER JOIN processing_activities pa ON pa.id = tia.processing_activity_id -WHERE tia.snapshot_id = @snapshot_id -ORDER BY pa.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", 0, fmt.Errorf("cannot load snapshot TIAs: %w", err) - } - defer rows.Close() - - var listRows []docgen.TransferImpactAssessmentListRow - for rows.Next() { - var name string - var dataSubjects, legalMechanism, transfer, localLawRisk, supplementary *string - if err := rows.Scan(&name, &dataSubjects, &legalMechanism, &transfer, &localLawRisk, &supplementary); err != nil { - return "", 0, fmt.Errorf("cannot scan TIA: %w", err) - } - listRows = append(listRows, docgen.TransferImpactAssessmentListRow{ - ProcessingActivityName: name, - DataSubjects: derefOrNotSpecified(dataSubjects), - LegalMechanism: derefOrNotSpecified(legalMechanism), - Transfer: derefOrNotSpecified(transfer), - LocalLawRisk: derefOrNotSpecified(localLawRisk), - SupplementaryMeasures: derefOrNotSpecified(supplementary), - }) - } - if err := rows.Err(); err != nil { - return "", 0, err - } - - if len(listRows) == 0 { - return "", 0, nil - } - - content, err := probo.BuildTransferImpactAssessmentListDocument(docgen.TransferImpactAssessmentListData{ - Title: "Transfer Impact Assessments", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalTransferImpactAssessments: len(listRows), - Rows: listRows, - }) - if err != nil { - return "", 0, err - } - return content, len(listRows), nil -} - -func derefOrNotSpecified(s *string) string { - if s == nil || *s == "" { - return "Not specified" - } - return *s -} - -func formatDateOrNotSpecified(t *time.Time) string { - if t == nil { - return "Not specified" - } - return t.Format("January 2, 2006") -} - -func yesNoLabel(b bool) string { - if b { - return "Yes" - } - return "No" -} - -func formatYesNoString(s string) string { - switch s { - case "NEEDED": - return "Yes" - case "NOT_NEEDED": - return "No" - default: - return s - } -} - -func formatRoleString(role string) string { - switch role { - case "CONTROLLER": - return "Controller" - case "PROCESSOR": - return "Processor" - default: - return role - } -} - -func formatLawfulBasisString(b string) string { - switch b { - case "CONSENT": - return "Consent" - case "CONTRACTUAL_NECESSITY": - return "Contractual Necessity" - case "LEGAL_OBLIGATION": - return "Legal Obligation" - case "LEGITIMATE_INTEREST": - return "Legitimate Interest" - case "PUBLIC_TASK": - return "Public Task" - case "VITAL_INTERESTS": - return "Vital Interests" - default: - return b - } -} - -func formatSpecialOrCriminalDataString(s string) string { - switch s { - case "YES": - return "Yes" - case "NO": - return "No" - case "POSSIBLE": - return "Possible" - default: - return s - } -} - -func formatTransferSafeguardString(s *string) string { - if s == nil { - return "Not specified" - } - switch *s { - case "STANDARD_CONTRACTUAL_CLAUSES": - return "Standard Contractual Clauses" - case "BINDING_CORPORATE_RULES": - return "Binding Corporate Rules" - case "ADEQUACY_DECISION": - return "Adequacy Decision" - case "DEROGATIONS": - return "Derogations" - case "CODES_OF_CONDUCT": - return "Codes of Conduct" - case "CERTIFICATION_MECHANISMS": - return "Certification Mechanisms" - default: - return *s - } -} - -func formatResidualRiskString(s *string) string { - if s == nil { - return "Not specified" - } - switch *s { - case "LOW": - return "Low" - case "MEDIUM": - return "Medium" - case "HIGH": - return "High" - default: - return *s - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN: %w", err) - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-risk-snapshots-to-documents/main.go b/cmd/migrate-risk-snapshots-to-documents/main.go deleted file mode 100644 index 32765ed81..000000000 --- a/cmd/migrate-risk-snapshots-to-documents/main.go +++ /dev/null @@ -1,530 +0,0 @@ -// 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. - -// Command migrate-risk-snapshots-to-documents creates documents and document -// versions from existing risk snapshots. For each organization that has risk -// snapshots, it generates a risk list document using the same ProseMirror -// builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return migrate(ctx, pgClient, dryRun) -} - -type orgWithRiskSnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type riskSnapshot struct { - snapshotID string - publishedAt time.Time -} - -func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error { - var orgs []orgWithRiskSnapshots - err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - var err error - orgs, err = loadOrgsWithRiskSnapshots(ctx, conn) - return err - }) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with risk snapshots to migrate") - return nil - } - - var stats struct { - documents, versions, failed int - } - - for _, org := range orgs { - if dryRun { - var count int - err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - snapshots, err := loadRiskSnapshots(ctx, conn, org.organizationID) - count = len(snapshots) - return err - }) - if err != nil { - return err - } - fmt.Printf("would migrate org %s (%s) — %d risk snapshot(s)\n", - org.organizationID, org.organizationName, count) - continue - } - - err := pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrateOrg(ctx, tx, org) - }) - if err != nil { - fmt.Fprintf(os.Stderr, "FAIL org %s (%s): %v\n", - org.organizationID, org.organizationName, err) - stats.failed++ - continue - } - - stats.documents++ - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\nmigrated %d organization(s), %d failed\n", - stats.documents, stats.failed) - - return nil -} - -func migrateOrg(ctx context.Context, tx pg.Tx, org orgWithRiskSnapshots) error { - snapshots, err := loadRiskSnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if len(snapshots) == 0 { - return nil - } - - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document: %w", err) - } - - _, err = tx.Exec( - ctx, - `INSERT INTO generated_documents (organization_id, tenant_id, risks_document_id, created_at, updated_at) -VALUES (@organization_id, @tenant_id, @risks_document_id, @created_at, @updated_at) -ON CONFLICT (organization_id) DO UPDATE SET risks_document_id = @risks_document_id, updated_at = @updated_at`, - pgx.NamedArgs{ - "organization_id": org.organizationID, - "tenant_id": org.tenantID, - "risks_document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document: %w", err) - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s: %w", snap.snapshotID, err) - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - pdf_attempt_count, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'PORTRAIT'::document_version_orientation, - 0, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": "Risks", - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - } - - fmt.Printf("OK org %s (%s) — %d version(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - - return nil -} - -func loadOrgsWithRiskSnapshots(ctx context.Context, conn pg.Querier) ([]orgWithRiskSnapshots, error) { - rows, err := conn.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id AND gd.risks_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM snapshots s - WHERE s.organization_id = o.id AND s.type = 'RISKS' - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with risk snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithRiskSnapshots - for rows.Next() { - var o orgWithRiskSnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadRiskSnapshots(ctx context.Context, conn pg.Querier, organizationID gid.GID) ([]riskSnapshot, error) { - rows, err := conn.Query( - ctx, - ` -SELECT DISTINCT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'RISKS' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query risk snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []riskSnapshot - for rows.Next() { - var s riskSnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -type riskInfo struct { - id string - name string - description *string - category string - treatment string - note string - ownerName string - inherentLikelihood int - inherentImpact int - inherentRiskScore int - residualLikelihood int - residualImpact int - residualRiskScore int -} - -func buildSnapshotContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, error) { - riskRows, err := tx.Query( - ctx, - ` -SELECT - r.id, - r.name, - r.description, - r.category, - r.treatment::text, - r.note, - COALESCE(NULLIF(p.full_name, ''), 'Not assigned'), - r.inherent_likelihood, - r.inherent_impact, - r.inherent_risk_score, - r.residual_likelihood, - r.residual_impact, - r.residual_risk_score -FROM risks r -LEFT JOIN iam_membership_profiles p ON p.id = r.owner_profile_id -WHERE r.snapshot_id = @snapshot_id -ORDER BY r.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot risks: %w", err) - } - defer riskRows.Close() - - var risks []riskInfo - for riskRows.Next() { - var r riskInfo - if err := riskRows.Scan( - &r.id, &r.name, &r.description, &r.category, &r.treatment, &r.note, - &r.ownerName, - &r.inherentLikelihood, &r.inherentImpact, &r.inherentRiskScore, - &r.residualLikelihood, &r.residualImpact, &r.residualRiskScore, - ); err != nil { - return "", fmt.Errorf("cannot scan risk: %w", err) - } - risks = append(risks, r) - } - if err := riskRows.Err(); err != nil { - return "", err - } - - rows := make([]docgen.RiskListRow, 0, len(risks)) - for _, r := range risks { - row := docgen.RiskListRow{ - Name: r.name, - Description: derefOrNotSpecified(r.description), - Category: stringOrNotSpecified(r.category), - Treatment: formatTreatment(r.treatment), - Owner: r.ownerName, - InherentLikelihood: r.inherentLikelihood, - InherentLikelihoodLabel: riskLikelihoodLabel(r.inherentLikelihood), - InherentImpact: r.inherentImpact, - InherentImpactLabel: riskImpactLabel(r.inherentImpact), - InherentRiskScore: r.inherentRiskScore, - InherentSeverity: riskSeverityLabel(r.inherentRiskScore), - ResidualLikelihood: r.residualLikelihood, - ResidualLikelihoodLabel: riskLikelihoodLabel(r.residualLikelihood), - ResidualImpact: r.residualImpact, - ResidualImpactLabel: riskImpactLabel(r.residualImpact), - ResidualRiskScore: r.residualRiskScore, - ResidualSeverity: riskSeverityLabel(r.residualRiskScore), - Note: stringOrNotSpecified(r.note), - } - rows = append(rows, row) - } - - docData := docgen.RiskListData{ - Title: "Risks", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalRisks: len(rows), - Rows: rows, - } - - return probo.BuildRiskListDocument(docData) -} - -func derefOrNotSpecified(s *string) string { - if s == nil || *s == "" { - return "Not specified" - } - return *s -} - -func stringOrNotSpecified(s string) string { - if s == "" { - return "Not specified" - } - return s -} - -func formatTreatment(t string) string { - switch t { - case "MITIGATED": - return "Mitigated" - case "ACCEPTED": - return "Accepted" - case "AVOIDED": - return "Avoided" - case "TRANSFERRED": - return "Transferred" - default: - return stringOrNotSpecified(t) - } -} - -func riskLikelihoodLabel(v int) string { - switch v { - case 1: - return "Improbable" - case 2: - return "Remote" - case 3: - return "Occasional" - case 4: - return "Probable" - case 5: - return "Frequent" - default: - return "Unknown" - } -} - -func riskImpactLabel(v int) string { - switch v { - case 1: - return "Negligible" - case 2: - return "Low" - case 3: - return "Moderate" - case 4: - return "Significant" - case 5: - return "Catastrophic" - default: - return "Unknown" - } -} - -func riskSeverityLabel(score int) string { - switch { - case score >= 15: - return "Critical" - case score >= 5: - return "High" - default: - return "Low" - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN") - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-soa-snapshots-to-documents/main.go b/cmd/migrate-soa-snapshots-to-documents/main.go deleted file mode 100644 index 1fbf4574d..000000000 --- a/cmd/migrate-soa-snapshots-to-documents/main.go +++ /dev/null @@ -1,539 +0,0 @@ -// 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. - -// Command migrate-soa-snapshots-to-documents creates documents, document -// versions, approval quorums, and approval decisions from existing SOA -// snapshots. For each snapshot it generates the ProseMirror content using -// the same builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrate(ctx, tx, dryRun) - }) -} - -type originalSOA struct { - id string - tenantID gid.TenantID - organizationID gid.GID - name string - ownerProfileID *string -} - -type snapshotSOA struct { - id string - snapshotID string - ownerProfileID *string - publishedAt time.Time -} - -func migrate(ctx context.Context, tx pg.Tx, dryRun bool) error { - originals, err := loadOriginalSOAs(ctx, tx) - if err != nil { - return err - } - - if len(originals) == 0 { - fmt.Println("no SOAs with snapshots to migrate") - return nil - } - - var stats struct { - documents, versions, quorums, decisions, defaultApprovers int - } - - for _, orig := range originals { - snapshots, err := loadSnapshots(ctx, tx, orig.id) - if err != nil { - return err - } - - if dryRun { - fmt.Printf("would migrate SOA %s (%s) — %d snapshot(s)\n", orig.id, orig.name, len(snapshots)) - continue - } - - documentID := gid.New(orig.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": orig.tenantID, - "organization_id": orig.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document for SOA %s: %w", orig.id, err) - } - stats.documents++ - - _, err = tx.Exec( - ctx, - `UPDATE statements_of_applicability SET document_id = @document_id WHERE id = @id`, - pgx.NamedArgs{ - "document_id": documentID, - "id": orig.id, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document to SOA %s: %w", orig.id, err) - } - - if orig.ownerProfileID != nil { - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_default_approvers ( - document_id, approver_profile_id, tenant_id, organization_id, created_at, updated_at -) VALUES ( - @document_id, @approver_profile_id, @tenant_id, @organization_id, @created_at, @created_at -)`, - pgx.NamedArgs{ - "document_id": documentID, - "approver_profile_id": *orig.ownerProfileID, - "tenant_id": orig.tenantID, - "organization_id": orig.organizationID, - "created_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert default approver for SOA %s: %w", orig.id, err) - } - stats.defaultApprovers++ - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.id, orig.name) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s of SOA %s: %w", snap.snapshotID, orig.id, err) - } - - versionID := gid.New(orig.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'STATEMENT_OF_APPLICABILITY'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'LANDSCAPE'::document_version_orientation, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": orig.tenantID, - "organization_id": orig.organizationID, - "document_id": documentID, - "title": orig.name, - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - stats.versions++ - - if snap.ownerProfileID != nil { - quorumID := gid.New(orig.tenantID, coredata.DocumentVersionApprovalQuorumEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_version_approval_quorums ( - id, tenant_id, organization_id, version_id, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @version_id, - 'APPROVED'::document_version_approval_quorum_status, - @created_at, @created_at -)`, - pgx.NamedArgs{ - "id": quorumID, - "tenant_id": orig.tenantID, - "organization_id": orig.organizationID, - "version_id": versionID, - "created_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert quorum for snapshot %s: %w", snap.snapshotID, err) - } - stats.quorums++ - - decisionID := gid.New(orig.tenantID, coredata.DocumentVersionApprovalDecisionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_version_approval_decisions ( - id, tenant_id, organization_id, quorum_id, - approver_id, state, decided_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @quorum_id, - @approver_id, - 'APPROVED'::document_version_approval_decision_state, - @decided_at, @decided_at, @decided_at -)`, - pgx.NamedArgs{ - "id": decisionID, - "tenant_id": orig.tenantID, - "organization_id": orig.organizationID, - "quorum_id": quorumID, - "approver_id": *snap.ownerProfileID, - "decided_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert decision for snapshot %s: %w", snap.snapshotID, err) - } - stats.decisions++ - } - } - - fmt.Printf("migrated SOA %s (%s) — %d version(s)\n", orig.id, orig.name, len(snapshots)) - } - - if dryRun { - fmt.Printf("\n%d SOA(s) would be migrated\n", len(originals)) - return nil - } - - fmt.Printf("\ncreated %d document(s), %d version(s), %d quorum(s), %d decision(s), %d default approver(s)\n", - stats.documents, stats.versions, stats.quorums, stats.decisions, stats.defaultApprovers) - - return nil -} - -func loadOriginalSOAs(ctx context.Context, tx pg.Tx) ([]originalSOA, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - soa.id, - soa.tenant_id, - soa.organization_id, - soa.name, - soa.owner_profile_id -FROM statements_of_applicability soa -WHERE soa.snapshot_id IS NULL - AND soa.document_id IS NULL - AND EXISTS ( - SELECT 1 FROM statements_of_applicability snap - WHERE snap.source_id = soa.id AND snap.snapshot_id IS NOT NULL - ) -ORDER BY soa.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query original SOAs: %w", err) - } - defer rows.Close() - - var result []originalSOA - for rows.Next() { - var o originalSOA - if err := rows.Scan(&o.id, &o.tenantID, &o.organizationID, &o.name, &o.ownerProfileID); err != nil { - return nil, fmt.Errorf("cannot scan original SOA: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadSnapshots(ctx context.Context, tx pg.Tx, originalSOAID string) ([]snapshotSOA, error) { - rows, err := tx.Query( - ctx, - ` -SELECT - snap.id, - snap.snapshot_id, - snap.owner_profile_id, - snap_record.created_at -FROM statements_of_applicability snap -JOIN snapshots snap_record ON snap_record.id = snap.snapshot_id -WHERE snap.source_id = @source_id - AND snap.snapshot_id IS NOT NULL -ORDER BY snap_record.created_at ASC; -`, - pgx.NamedArgs{"source_id": originalSOAID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query snapshots for SOA %s: %w", originalSOAID, err) - } - defer rows.Close() - - var result []snapshotSOA - for rows.Next() { - var s snapshotSOA - if err := rows.Scan(&s.id, &s.snapshotID, &s.ownerProfileID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -type snapshotControl struct { - frameworkName string - sectionTitle string - controlName string - applicability bool - justification *string - bestPractice bool - maturityLevel string - notImplementedJustification *string - hasLegal bool - hasContractual bool - hasRisk bool -} - -func buildSnapshotContent(ctx context.Context, tx pg.Tx, snapshotSOAID string, soaName string) (string, error) { - rows, err := tx.Query( - ctx, - ` -WITH control_risks_via_measures AS ( - SELECT DISTINCT cm.control_id - FROM controls_measures cm - INNER JOIN risks_measures rm ON cm.measure_id = rm.measure_id -), -control_risks_via_documents AS ( - SELECT DISTINCT cd.control_id - FROM controls_documents cd - INNER JOIN risks_documents rd ON cd.document_id = rd.document_id -), -control_risks AS ( - SELECT control_id FROM control_risks_via_measures - UNION - SELECT control_id FROM control_risks_via_documents -) -SELECT - f.name AS framework_name, - c.section_title, - c.name AS control_name, - stmt.applicability, - stmt.justification, - c.best_practice, - c.maturity_level, - c.not_implemented_justification, - EXISTS ( - SELECT 1 FROM controls_obligations co - JOIN obligations o ON o.id = co.obligation_id - WHERE co.control_id = c.id AND o.type = 'LEGAL' - ) AS has_legal, - EXISTS ( - SELECT 1 FROM controls_obligations co - JOIN obligations o ON o.id = co.obligation_id - WHERE co.control_id = c.id AND o.type = 'CONTRACTUAL' - ) AS has_contractual, - EXISTS ( - SELECT 1 FROM control_risks cr WHERE cr.control_id = c.id - ) AS has_risk -FROM applicability_statements stmt -JOIN controls c ON c.id = stmt.control_id -JOIN frameworks f ON f.id = c.framework_id -WHERE stmt.statement_of_applicability_id = @soa_id -ORDER BY f.name, c.section_title; -`, - pgx.NamedArgs{"soa_id": snapshotSOAID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot controls: %w", err) - } - defer rows.Close() - - var soaRows []docgen.SOARow - - for rows.Next() { - var sc snapshotControl - if err := rows.Scan( - &sc.frameworkName, - &sc.sectionTitle, - &sc.controlName, - &sc.applicability, - &sc.justification, - &sc.bestPractice, - &sc.maturityLevel, - &sc.notImplementedJustification, - &sc.hasLegal, - &sc.hasContractual, - &sc.hasRisk, - ); err != nil { - return "", fmt.Errorf("cannot scan control: %w", err) - } - - applicable := sc.applicability - - justification := "-" - if !applicable && sc.justification != nil { - justification = *sc.justification - } - - maturityLevel := "-" - if applicable { - maturityLevel = docgen.MaturityLabel(coredata.ControlMaturityLevel(sc.maturityLevel)) - } - - notImplJustification := "-" - if applicable && sc.maturityLevel == "NONE" && sc.notImplementedJustification != nil { - notImplJustification = *sc.notImplementedJustification - } - - regulatory := "-" - contractual := "-" - bestPractice := "-" - riskAssessment := "-" - if applicable { - regulatory = docgen.BoolLabel(sc.hasLegal) - contractual = docgen.BoolLabel(sc.hasContractual) - bestPractice = docgen.BoolLabel(sc.bestPractice) - riskAssessment = docgen.BoolLabel(sc.hasRisk) - } - - soaRows = append(soaRows, docgen.SOARow{ - FrameworkName: sc.frameworkName, - ControlSection: sc.sectionTitle, - ControlName: sc.controlName, - Applicability: docgen.BoolLabel(applicable), - Justification: justification, - MaturityLevel: maturityLevel, - NotImplJustification: notImplJustification, - Regulatory: regulatory, - Contractual: contractual, - BestPractice: bestPractice, - RiskAssessment: riskAssessment, - }) - } - - if err := rows.Err(); err != nil { - return "", err - } - - data := docgen.StatementOfApplicabilityData{ - Title: soaName, - TotalControls: len(soaRows), - Rows: soaRows, - } - - return probo.BuildStatementOfApplicabilityDocument(data) -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN") - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/cmd/migrate-third-party-snapshots-to-documents/main.go b/cmd/migrate-third-party-snapshots-to-documents/main.go deleted file mode 100644 index 53c241a36..000000000 --- a/cmd/migrate-third-party-snapshots-to-documents/main.go +++ /dev/null @@ -1,754 +0,0 @@ -// 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. - -// Command migrate-thirdParty-snapshots-to-documents creates documents and document -// versions from existing thirdParty snapshots. For each organization that has thirdParty -// snapshots, it generates a thirdParty list document using the same ProseMirror -// builder as the publish flow. -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/url" - "os" - "strings" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/docgen" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" -) - -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run() error { - var ( - pgDSN string - dryRun bool - ) - - flag.StringVar( - &pgDSN, - "pg-dsn", - os.Getenv("DATABASE_URL"), - "PostgreSQL connection URL (default: DATABASE_URL env)", - ) - flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing") - flag.Parse() - - if pgDSN == "" { - return fmt.Errorf("set -pg-dsn or DATABASE_URL") - } - - ctx := context.Background() - - pgClient, err := newPgClientFromDSN(pgDSN) - if err != nil { - return fmt.Errorf("cannot create pg client: %w", err) - } - - return migrate(ctx, pgClient, dryRun) -} - -type orgWithThirdPartySnapshots struct { - organizationID gid.GID - tenantID gid.TenantID - organizationName string -} - -type thirdPartySnapshot struct { - snapshotID string - publishedAt time.Time -} - -func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error { - var orgs []orgWithThirdPartySnapshots - err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - var err error - orgs, err = loadOrgsWithThirdPartySnapshots(ctx, conn) - return err - }) - if err != nil { - return err - } - - if len(orgs) == 0 { - fmt.Println("no organizations with thirdParty snapshots to migrate") - return nil - } - - var stats struct { - documents, versions, failed int - } - - for _, org := range orgs { - if dryRun { - var count int - err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - snapshots, err := loadThirdPartySnapshots(ctx, conn, org.organizationID) - count = len(snapshots) - return err - }) - if err != nil { - return err - } - fmt.Printf("would migrate org %s (%s) — %d thirdParty snapshot(s)\n", - org.organizationID, org.organizationName, count) - continue - } - - err := pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return migrateOrg(ctx, tx, org) - }) - if err != nil { - fmt.Fprintf(os.Stderr, "FAIL org %s (%s): %v\n", - org.organizationID, org.organizationName, err) - stats.failed++ - continue - } - - stats.documents++ - } - - if dryRun { - fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs)) - return nil - } - - fmt.Printf("\nmigrated %d organization(s), %d failed\n", - stats.documents, stats.failed) - - return nil -} - -func migrateOrg(ctx context.Context, tx pg.Tx, org orgWithThirdPartySnapshots) error { - snapshots, err := loadThirdPartySnapshots(ctx, tx, org.organizationID) - if err != nil { - return err - } - - if len(snapshots) == 0 { - return nil - } - - documentID := gid.New(org.tenantID, coredata.DocumentEntityType) - now := time.Now() - - _, err = tx.Exec( - ctx, - ` -INSERT INTO documents ( - id, tenant_id, organization_id, write_mode, - current_published_major, current_published_minor, - trust_center_visibility, status, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, - 'GENERATED'::document_write_mode, - @current_published_major, 0, - 'NONE'::trust_center_visibility, - 'ACTIVE'::document_status, - @created_at, @updated_at -)`, - pgx.NamedArgs{ - "id": documentID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "current_published_major": len(snapshots), - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert document: %w", err) - } - - _, err = tx.Exec( - ctx, - `INSERT INTO generated_documents (organization_id, tenant_id, third_parties_document_id, created_at, updated_at) -VALUES (@organization_id, @tenant_id, @third_parties_document_id, @created_at, @updated_at) -ON CONFLICT (organization_id) DO UPDATE SET third_parties_document_id = @third_parties_document_id, updated_at = @updated_at`, - pgx.NamedArgs{ - "organization_id": org.organizationID, - "tenant_id": org.tenantID, - "third_parties_document_id": documentID, - "created_at": now, - "updated_at": now, - }, - ) - if err != nil { - return fmt.Errorf("cannot link document: %w", err) - } - - for major, snap := range snapshots { - content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt) - if err != nil { - return fmt.Errorf("cannot build content for snapshot %s: %w", snap.snapshotID, err) - } - - versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType) - - _, err = tx.Exec( - ctx, - ` -INSERT INTO document_versions ( - id, tenant_id, organization_id, document_id, - title, major, minor, classification, document_type, - content, changelog, status, orientation, - pdf_attempt_count, - published_at, created_at, updated_at -) VALUES ( - @id, @tenant_id, @organization_id, @document_id, - @title, @major, 0, - 'CONFIDENTIAL'::document_classification, - 'REGISTER'::document_type, - @content, '', - 'PUBLISHED'::document_version_status, - 'PORTRAIT'::document_version_orientation, - 0, - @published_at, @published_at, @published_at -)`, - pgx.NamedArgs{ - "id": versionID, - "tenant_id": org.tenantID, - "organization_id": org.organizationID, - "document_id": documentID, - "title": "ThirdParties", - "major": major + 1, - "content": content, - "published_at": snap.publishedAt, - }, - ) - if err != nil { - return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err) - } - } - - fmt.Printf("OK org %s (%s) — %d version(s)\n", - org.organizationID, org.organizationName, len(snapshots)) - - return nil -} - -func loadOrgsWithThirdPartySnapshots(ctx context.Context, conn pg.Querier) ([]orgWithThirdPartySnapshots, error) { - rows, err := conn.Query( - ctx, - ` -SELECT DISTINCT - o.id, - o.tenant_id, - o.name, - o.created_at -FROM organizations o -WHERE NOT EXISTS ( - SELECT 1 FROM generated_documents gd - WHERE gd.organization_id = o.id AND gd.third_parties_document_id IS NOT NULL - ) - AND EXISTS ( - SELECT 1 FROM snapshots s - WHERE s.organization_id = o.id AND s.type = 'VENDORS' - ) -ORDER BY o.created_at; -`, - ) - if err != nil { - return nil, fmt.Errorf("cannot query organizations with thirdParty snapshots: %w", err) - } - defer rows.Close() - - var result []orgWithThirdPartySnapshots - for rows.Next() { - var o orgWithThirdPartySnapshots - var createdAt time.Time - if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { - return nil, fmt.Errorf("cannot scan organization: %w", err) - } - result = append(result, o) - } - - return result, rows.Err() -} - -func loadThirdPartySnapshots(ctx context.Context, conn pg.Querier, organizationID gid.GID) ([]thirdPartySnapshot, error) { - rows, err := conn.Query( - ctx, - ` -SELECT DISTINCT - s.id, - s.created_at -FROM snapshots s -WHERE s.organization_id = @organization_id - AND s.type = 'VENDORS' -ORDER BY s.created_at ASC; -`, - pgx.NamedArgs{"organization_id": organizationID}, - ) - if err != nil { - return nil, fmt.Errorf("cannot query thirdParty snapshots for org %s: %w", organizationID, err) - } - defer rows.Close() - - var result []thirdPartySnapshot - for rows.Next() { - var s thirdPartySnapshot - if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { - return nil, fmt.Errorf("cannot scan snapshot: %w", err) - } - result = append(result, s) - } - - return result, rows.Err() -} - -type thirdPartyInfo struct { - id string - name string - category string - - legalName *string - description *string - headquarterAddress *string - websiteURL *string - privacyPolicyURL *string - serviceLevelAgreementURL *string - dataProcessingAgreementURL *string - businessAssociateAgreementURL *string - subprocessorsListURL *string - statusPageURL *string - termsOfServiceURL *string - securityPageURL *string - trustPageURL *string - certifications []string - countries []string - businessOwnerName string - securityOwnerName string -} - -func buildSnapshotContent( - ctx context.Context, - tx pg.Tx, - snapshotID string, - orgName string, - publishedAt time.Time, -) (string, error) { - thirdPartyRows, err := tx.Query( - ctx, - ` -SELECT - v.id, - v.name, - v.category, - v.legal_name, - v.description, - v.headquarter_address, - v.website_url, - v.privacy_policy_url, - v.service_level_agreement_url, - v.data_processing_agreement_url, - v.business_associate_agreement_url, - v.subprocessors_list_url, - v.status_page_url, - v.terms_of_service_url, - v.security_page_url, - v.trust_page_url, - v.certifications, - v.countries, - COALESCE(bo.full_name, 'Not assigned'), - COALESCE(so.full_name, 'Not assigned') -FROM third_parties v -LEFT JOIN iam_membership_profiles bo ON bo.id = v.business_owner_profile_id -LEFT JOIN iam_membership_profiles so ON so.id = v.security_owner_profile_id -WHERE v.snapshot_id = @snapshot_id -ORDER BY v.name ASC; -`, - pgx.NamedArgs{"snapshot_id": snapshotID}, - ) - if err != nil { - return "", fmt.Errorf("cannot load snapshot thirdParties: %w", err) - } - defer thirdPartyRows.Close() - - var thirdParties []thirdPartyInfo - for thirdPartyRows.Next() { - var v thirdPartyInfo - if err := thirdPartyRows.Scan( - &v.id, &v.name, &v.category, - &v.legalName, &v.description, &v.headquarterAddress, - &v.websiteURL, &v.privacyPolicyURL, &v.serviceLevelAgreementURL, - &v.dataProcessingAgreementURL, &v.businessAssociateAgreementURL, - &v.subprocessorsListURL, &v.statusPageURL, &v.termsOfServiceURL, - &v.securityPageURL, &v.trustPageURL, - &v.certifications, &v.countries, - &v.businessOwnerName, &v.securityOwnerName, - ); err != nil { - return "", fmt.Errorf("cannot scan thirdParty: %w", err) - } - thirdParties = append(thirdParties, v) - } - if err := thirdPartyRows.Err(); err != nil { - return "", err - } - - thirdPartyIDs := make([]string, len(thirdParties)) - for i, v := range thirdParties { - thirdPartyIDs[i] = v.id - } - - servicesByThirdParty, err := loadSnapshotServices(ctx, tx, snapshotID, thirdPartyIDs) - if err != nil { - return "", err - } - - contactsByThirdParty, err := loadSnapshotContacts(ctx, tx, snapshotID, thirdPartyIDs) - if err != nil { - return "", err - } - - assessmentsByThirdParty, err := loadSnapshotRiskAssessments(ctx, tx, snapshotID, thirdPartyIDs) - if err != nil { - return "", err - } - - reportsByThirdParty, err := loadSnapshotComplianceReports(ctx, tx, snapshotID, thirdPartyIDs) - if err != nil { - return "", err - } - - baaByThirdParty, err := loadSnapshotBAAs(ctx, tx, snapshotID, thirdPartyIDs) - if err != nil { - return "", err - } - - dpaByThirdParty, err := loadSnapshotDPAs(ctx, tx, snapshotID, thirdPartyIDs) - if err != nil { - return "", err - } - - rows := make([]docgen.ThirdPartyListRow, 0, len(thirdParties)) - for _, v := range thirdParties { - row := docgen.ThirdPartyListRow{ - Name: v.name, - LegalName: deref(v.legalName), - Description: deref(v.description), - Category: formatCategory(v.category), - HeadquarterAddress: deref(v.headquarterAddress), - WebsiteURL: deref(v.websiteURL), - PrivacyPolicyURL: deref(v.privacyPolicyURL), - ServiceLevelAgreementURL: deref(v.serviceLevelAgreementURL), - DataProcessingAgreementURL: deref(v.dataProcessingAgreementURL), - BusinessAssociateAgreementURL: deref(v.businessAssociateAgreementURL), - SubprocessorsListURL: deref(v.subprocessorsListURL), - StatusPageURL: deref(v.statusPageURL), - TermsOfServiceURL: deref(v.termsOfServiceURL), - SecurityPageURL: deref(v.securityPageURL), - TrustPageURL: deref(v.trustPageURL), - Certifications: joinOrDefault(v.certifications), - Countries: joinOrDefault(v.countries), - BusinessOwner: v.businessOwnerName, - SecurityOwner: v.securityOwnerName, - Services: servicesByThirdParty[v.id], - Contacts: contactsByThirdParty[v.id], - RiskAssessments: assessmentsByThirdParty[v.id], - ComplianceReports: reportsByThirdParty[v.id], - BusinessAssociateAgreement: baaByThirdParty[v.id], - DataPrivacyAgreement: dpaByThirdParty[v.id], - } - rows = append(rows, row) - } - - docData := docgen.ThirdPartyListData{ - Title: "ThirdParties", - OrganizationName: orgName, - CreatedAt: publishedAt, - TotalThirdParties: len(rows), - Rows: rows, - } - - return probo.BuildThirdPartyListDocument(docData) -} - -func loadSnapshotServices(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListService, error) { - rows, err := tx.Query(ctx, - `SELECT vs.third_party_id, vs.name, COALESCE(vs.description, 'Not specified') - FROM third_party_services vs - WHERE vs.snapshot_id = @snapshot_id AND vs.third_party_id = ANY(@third_party_ids) - ORDER BY vs.third_party_id, vs.name ASC`, - pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs}) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot services: %w", err) - } - defer rows.Close() - - result := make(map[string][]docgen.ThirdPartyListService) - for rows.Next() { - var thirdPartyID, name, desc string - if err := rows.Scan(&thirdPartyID, &name, &desc); err != nil { - return nil, fmt.Errorf("cannot scan service: %w", err) - } - result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListService{Name: name, Description: desc}) - } - return result, rows.Err() -} - -func loadSnapshotContacts(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListContact, error) { - rows, err := tx.Query(ctx, - `SELECT vc.third_party_id, - COALESCE(vc.full_name, 'Not specified'), - COALESCE(vc.email, 'Not specified'), - COALESCE(vc.phone, 'Not specified'), - COALESCE(vc.role, 'Not specified') - FROM third_party_contacts vc - WHERE vc.snapshot_id = @snapshot_id AND vc.third_party_id = ANY(@third_party_ids) - ORDER BY vc.third_party_id, vc.full_name ASC`, - pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs}) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot contacts: %w", err) - } - defer rows.Close() - - result := make(map[string][]docgen.ThirdPartyListContact) - for rows.Next() { - var thirdPartyID, name, email, phone, role string - if err := rows.Scan(&thirdPartyID, &name, &email, &phone, &role); err != nil { - return nil, fmt.Errorf("cannot scan contact: %w", err) - } - result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListContact{ - FullName: name, Email: email, Phone: phone, Role: role, - }) - } - return result, rows.Err() -} - -func loadSnapshotRiskAssessments(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListRiskAssessment, error) { - rows, err := tx.Query(ctx, - `SELECT vra.third_party_id, vra.created_at, vra.expires_at, vra.data_sensitivity, vra.business_impact, COALESCE(vra.notes, 'Not specified') - FROM third_party_risk_assessments vra - WHERE vra.snapshot_id = @snapshot_id AND vra.third_party_id = ANY(@third_party_ids) - ORDER BY vra.third_party_id, vra.created_at DESC`, - pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs}) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot risk assessments: %w", err) - } - defer rows.Close() - - result := make(map[string][]docgen.ThirdPartyListRiskAssessment) - for rows.Next() { - var thirdPartyID, sensitivity, impact, notes string - var assessedAt, expiresAt time.Time - if err := rows.Scan(&thirdPartyID, &assessedAt, &expiresAt, &sensitivity, &impact, ¬es); err != nil { - return nil, fmt.Errorf("cannot scan risk assessment: %w", err) - } - result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListRiskAssessment{ - AssessedAt: assessedAt.Format("2006-01-02"), - ExpiresAt: expiresAt.Format("2006-01-02"), - DataSensitivity: sensitivity, - BusinessImpact: impact, - Notes: notes, - }) - } - return result, rows.Err() -} - -func loadSnapshotComplianceReports(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListComplianceReport, error) { - rows, err := tx.Query(ctx, - `SELECT vcr.third_party_id, vcr.report_name, vcr.report_date, vcr.valid_until - FROM third_party_compliance_reports vcr - WHERE vcr.snapshot_id = @snapshot_id AND vcr.third_party_id = ANY(@third_party_ids) - ORDER BY vcr.third_party_id, vcr.report_date DESC`, - pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs}) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot compliance reports: %w", err) - } - defer rows.Close() - - result := make(map[string][]docgen.ThirdPartyListComplianceReport) - for rows.Next() { - var thirdPartyID, name string - var reportDate time.Time - var validUntil *time.Time - if err := rows.Scan(&thirdPartyID, &name, &reportDate, &validUntil); err != nil { - return nil, fmt.Errorf("cannot scan compliance report: %w", err) - } - vu := "Not specified" - if validUntil != nil { - vu = validUntil.Format("2006-01-02") - } - result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListComplianceReport{ - ReportName: name, ReportDate: reportDate.Format("2006-01-02"), ValidUntil: vu, - }) - } - return result, rows.Err() -} - -func loadSnapshotBAAs(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string]*docgen.ThirdPartyListAgreement, error) { - rows, err := tx.Query(ctx, - `SELECT vbaa.third_party_id, vbaa.valid_from, vbaa.valid_until - FROM third_party_business_associate_agreements vbaa - WHERE vbaa.snapshot_id = @snapshot_id AND vbaa.third_party_id = ANY(@third_party_ids)`, - pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs}) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot BAAs: %w", err) - } - defer rows.Close() - - result := make(map[string]*docgen.ThirdPartyListAgreement) - for rows.Next() { - var thirdPartyID string - var validFrom, validUntil *time.Time - if err := rows.Scan(&thirdPartyID, &validFrom, &validUntil); err != nil { - return nil, fmt.Errorf("cannot scan BAA: %w", err) - } - result[thirdPartyID] = &docgen.ThirdPartyListAgreement{ - ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil), - } - } - return result, rows.Err() -} - -func loadSnapshotDPAs(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string]*docgen.ThirdPartyListAgreement, error) { - rows, err := tx.Query(ctx, - `SELECT vdpa.third_party_id, vdpa.valid_from, vdpa.valid_until - FROM third_party_data_privacy_agreements vdpa - WHERE vdpa.snapshot_id = @snapshot_id AND vdpa.third_party_id = ANY(@third_party_ids)`, - pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs}) - if err != nil { - return nil, fmt.Errorf("cannot load snapshot DPAs: %w", err) - } - defer rows.Close() - - result := make(map[string]*docgen.ThirdPartyListAgreement) - for rows.Next() { - var thirdPartyID string - var validFrom, validUntil *time.Time - if err := rows.Scan(&thirdPartyID, &validFrom, &validUntil); err != nil { - return nil, fmt.Errorf("cannot scan DPA: %w", err) - } - result[thirdPartyID] = &docgen.ThirdPartyListAgreement{ - ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil), - } - } - return result, rows.Err() -} - -func deref(s *string) string { - if s == nil || *s == "" { - return "Not specified" - } - return *s -} - -func joinOrDefault(items []string) string { - if len(items) == 0 { - return "Not specified" - } - return strings.Join(items, ", ") -} - -func fmtTime(t *time.Time) string { - if t == nil { - return "Not specified" - } - return t.Format("2006-01-02") -} - -func formatCategory(c string) string { - switch c { - case "ANALYTICS": - return "Analytics" - case "CLOUD_MONITORING": - return "Cloud Monitoring" - case "CLOUD_PROVIDER": - return "Cloud Provider" - case "COLLABORATION": - return "Collaboration" - case "CUSTOMER_SUPPORT": - return "Customer Support" - case "DATA_STORAGE_AND_PROCESSING": - return "Data Storage and Processing" - case "DOCUMENT_MANAGEMENT": - return "Document Management" - case "EMPLOYEE_MANAGEMENT": - return "Employee Management" - case "ENGINEERING": - return "Engineering" - case "FINANCE": - return "Finance" - case "IDENTITY_PROVIDER": - return "Identity Provider" - case "IT": - return "IT" - case "MARKETING": - return "Marketing" - case "OFFICE_OPERATIONS": - return "Office Operations" - case "OTHER": - return "Other" - case "PASSWORD_MANAGEMENT": - return "Password Management" - case "PRODUCT_AND_DESIGN": - return "Product and Design" - case "PROFESSIONAL_SERVICES": - return "Professional Services" - case "RECRUITING": - return "Recruiting" - case "SALES": - return "Sales" - case "SECURITY": - return "Security" - case "VERSION_CONTROL": - return "Version Control" - default: - return c - } -} - -func newPgClientFromDSN(dsn string) (*pg.Client, error) { - u, err := url.Parse(dsn) - if err != nil { - return nil, fmt.Errorf("cannot parse DSN") - } - - var opts []pg.Option - - if u.Host != "" { - host := u.Host - if u.Port() == "" { - host = net.JoinHostPort(u.Hostname(), "5432") - } - opts = append(opts, pg.WithAddr(host)) - } - - if u.User != nil { - opts = append(opts, pg.WithUser(u.User.Username())) - if password, ok := u.User.Password(); ok { - opts = append(opts, pg.WithPassword(password)) - } - } - - if len(u.Path) > 1 { - opts = append(opts, pg.WithDatabase(u.Path[1:])) - } - - return pg.NewClient(opts...) -} diff --git a/contrib/claude/coredata.md b/contrib/claude/coredata.md index 80bc5c6b3..f74d87e31 100644 --- a/contrib/claude/coredata.md +++ b/contrib/claude/coredata.md @@ -13,7 +13,6 @@ Every entity uses `gid.GID` for its ID, `db` tags for pgx mapping, and `CreatedA type ( Asset struct { ID gid.GID `db:"id"` - SnapshotID *gid.GID `db:"snapshot_id"` Name string `db:"name"` OrganizationID gid.GID `db:"organization_id"` AssetType AssetType `db:"asset_type"` diff --git a/pkg/coredata/applicability_statement.go b/pkg/coredata/applicability_statement.go index c352bb8f0..30c575fa8 100644 --- a/pkg/coredata/applicability_statement.go +++ b/pkg/coredata/applicability_statement.go @@ -34,7 +34,6 @@ type ( StatementOfApplicabilityID gid.GID `db:"statement_of_applicability_id"` ControlID gid.GID `db:"control_id"` OrganizationID gid.GID `db:"organization_id"` - SnapshotID *gid.GID `db:"snapshot_id"` Applicability bool `db:"applicability"` Justification *string `db:"justification"` CreatedAt time.Time `db:"created_at"` @@ -85,7 +84,6 @@ WITH stmt AS ( a.statement_of_applicability_id, a.control_id, a.organization_id, - a.snapshot_id, a.applicability, a.justification, a.created_at, @@ -107,7 +105,6 @@ SELECT statement_of_applicability_id, control_id, organization_id, - snapshot_id, applicability, justification, created_at, @@ -162,7 +159,6 @@ SELECT soac.statement_of_applicability_id, soac.control_id, soac.organization_id, - soac.snapshot_id, soac.applicability, soac.justification, soac.created_at, @@ -218,7 +214,6 @@ INSERT INTO control_id, organization_id, tenant_id, - snapshot_id, applicability, justification, created_at, @@ -230,7 +225,6 @@ VALUES ( @control_id, @organization_id, @tenant_id, - @snapshot_id, @applicability, @justification, @created_at, @@ -244,7 +238,6 @@ VALUES ( "control_id": sac.ControlID, "organization_id": sac.OrganizationID, "tenant_id": scope.GetTenantID(), - "snapshot_id": sac.SnapshotID, "applicability": sac.Applicability, "justification": sac.Justification, "created_at": sac.CreatedAt, @@ -410,7 +403,6 @@ WITH stmt AS ( a.statement_of_applicability_id, a.control_id, a.organization_id, - a.snapshot_id, a.applicability, a.justification, a.created_at, @@ -432,7 +424,6 @@ SELECT statement_of_applicability_id, control_id, organization_id, - snapshot_id, applicability, justification, created_at, @@ -477,7 +468,6 @@ SELECT a.statement_of_applicability_id, a.control_id, a.organization_id, - a.snapshot_id, a.applicability, a.justification, a.created_at, @@ -558,7 +548,6 @@ WITH soac_ctrl AS ( soac.statement_of_applicability_id, soac.control_id, soac.organization_id, - soac.snapshot_id, soac.applicability, soac.justification, soac.created_at, @@ -576,14 +565,12 @@ WITH soac_ctrl AS ( WHERE soac.%[1]s AND soac.control_id = @control_id - AND soa.snapshot_id IS NULL ) SELECT id, statement_of_applicability_id, control_id, organization_id, - snapshot_id, applicability, justification, created_at, diff --git a/pkg/coredata/asset.go b/pkg/coredata/asset.go index b1cde4f2b..e60dbb72b 100644 --- a/pkg/coredata/asset.go +++ b/pkg/coredata/asset.go @@ -91,7 +91,6 @@ FROM WHERE %s AND id = @asset_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -140,7 +139,6 @@ FROM WHERE %s AND owner_profile_id = @owner_profile_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -182,7 +180,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -223,7 +220,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -270,7 +266,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ORDER BY name ASC ` @@ -364,7 +359,6 @@ SET WHERE %s AND id = @id - AND snapshot_id IS NULL RETURNING id, name, @@ -415,7 +409,6 @@ DELETE FROM assets WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/data_protection_impact_assessment.go b/pkg/coredata/data_protection_impact_assessment.go index 042cdb004..14964eae9 100644 --- a/pkg/coredata/data_protection_impact_assessment.go +++ b/pkg/coredata/data_protection_impact_assessment.go @@ -138,8 +138,6 @@ WHERE type ( DataProtectionImpactAssessment struct { ID gid.GID `db:"id"` - SnapshotID *gid.GID `db:"snapshot_id"` - SourceID *gid.GID `db:"source_id"` OrganizationID gid.GID `db:"organization_id"` ProcessingActivityID gid.GID `db:"processing_activity_id"` Description *string `db:"description"` @@ -192,7 +190,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -221,8 +218,6 @@ func (dpias *DataProtectionImpactAssessments) LoadByOrganizationID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, description, @@ -237,7 +232,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -271,8 +265,6 @@ func (dpias *DataProtectionImpactAssessments) LoadAllByOrganizationID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, description, @@ -287,7 +279,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -319,8 +310,6 @@ func (dpia *DataProtectionImpactAssessment) LoadByID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, description, @@ -371,8 +360,6 @@ func (dpia *DataProtectionImpactAssessment) LoadByProcessingActivityID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, description, diff --git a/pkg/coredata/datum.go b/pkg/coredata/datum.go index d33851643..105362de1 100644 --- a/pkg/coredata/datum.go +++ b/pkg/coredata/datum.go @@ -89,7 +89,6 @@ FROM WHERE %s AND id = @data_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -132,7 +131,6 @@ FROM WHERE %s AND owner_profile_id = @owner_profile_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -170,7 +168,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -210,7 +207,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -255,7 +251,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ORDER BY name ASC ` @@ -341,7 +336,6 @@ SET WHERE %s AND id = @id - AND snapshot_id IS NULL RETURNING id, name, @@ -388,7 +382,6 @@ DELETE FROM data WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/finding.go b/pkg/coredata/finding.go index 31b694025..df28ebbb7 100644 --- a/pkg/coredata/finding.go +++ b/pkg/coredata/finding.go @@ -31,8 +31,6 @@ 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"` @@ -98,8 +96,6 @@ func (f *Finding) LoadByID( SELECT id, organization_id, - snapshot_id, - source_id, kind, reference_id, description, @@ -120,7 +116,6 @@ FROM WHERE %s AND id = @finding_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -159,7 +154,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -192,8 +186,6 @@ func (fs *Findings) LoadByOrganizationID( SELECT id, organization_id, - snapshot_id, - source_id, kind, reference_id, description, @@ -214,7 +206,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s AND %s ` @@ -264,7 +255,7 @@ WITH next_ref AS ( 0 ) + 1 AS next_num FROM findings - WHERE organization_id = @organization_id AND snapshot_id IS NULL + WHERE organization_id = @organization_id ) INSERT INTO findings ( id, @@ -360,7 +351,6 @@ SET WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -399,7 +389,7 @@ func (f *Finding) Delete( DELETE FROM findings WHERE %s - AND id = @id AND snapshot_id IS NULL + AND id = @id ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -429,8 +419,6 @@ WITH f AS ( fi.id, fi.tenant_id, fi.organization_id, - fi.snapshot_id, - fi.source_id, fi.kind, fi.reference_id, fi.description, @@ -452,13 +440,10 @@ 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, organization_id, - snapshot_id, - source_id, kind, reference_id, description, @@ -520,7 +505,6 @@ 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) @@ -558,8 +542,6 @@ func (fs *Findings) LoadAllByOrganizationID( SELECT id, organization_id, - snapshot_id, - source_id, kind, reference_id, description, @@ -580,7 +562,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ORDER BY reference_id ASC ` diff --git a/pkg/coredata/migrations/20260506T150405Z.sql b/pkg/coredata/migrations/20260506T150405Z.sql new file mode 100644 index 000000000..15f46930f --- /dev/null +++ b/pkg/coredata/migrations/20260506T150405Z.sql @@ -0,0 +1,30 @@ +-- 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. + +-- The register/document model has fully replaced the snapshot system. Delete the +-- snapshot-scoped data; the schema (snapshot_id / source_id columns, snapshots +-- table, controls_snapshots, snapshots_type enum, snapshot-scoped indexes) is +-- dropped in a follow-up migration. + +-- processing_activity_vendors stores snapshot_id without an FK to snapshots, so +-- the cascade delete below would not reach it. Clean it up explicitly first. +DELETE FROM processing_activity_vendors WHERE snapshot_id IS NOT NULL; + +-- Every other table with a snapshot_id has a FOREIGN KEY (snapshot_id) REFERENCES +-- snapshots(id) ON DELETE CASCADE, so deleting all snapshots removes every +-- snapshot-scoped row across data, vendors, assets, risks, findings, obligations, +-- processing_activities, statements_of_applicability, applicability_statements, +-- the vendor_* sub-tables, the processing_activity DPIA/TIA tables, and the +-- controls_snapshots junction. +DELETE FROM snapshots; diff --git a/pkg/coredata/obligation.go b/pkg/coredata/obligation.go index d78c9be2e..d01dbb35b 100644 --- a/pkg/coredata/obligation.go +++ b/pkg/coredata/obligation.go @@ -41,8 +41,6 @@ type ( DueDate *time.Time `db:"due_date"` Status ObligationStatus `db:"status"` Type ObligationType `db:"type"` - SnapshotID *gid.GID `db:"snapshot_id"` - SourceID *gid.GID `db:"source_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } @@ -89,8 +87,6 @@ func (o *Obligation) LoadByID( SELECT id, organization_id, - snapshot_id, - source_id, area, source, requirement, @@ -108,7 +104,6 @@ FROM WHERE %s AND id = @obligation_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -146,7 +141,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -176,7 +170,6 @@ WITH obls AS ( SELECT o.id, o.tenant_id, - o.snapshot_id, o.search_vector FROM obligations o @@ -184,7 +177,6 @@ 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) @@ -230,8 +222,6 @@ SELECT due_date, status, type, - snapshot_id, - source_id, created_at, updated_at FROM @@ -239,7 +229,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -286,8 +275,6 @@ WITH obls AS ( o.due_date, o.status, o.type, - o.snapshot_id, - o.source_id, o.created_at, o.updated_at, o.tenant_id, @@ -298,7 +285,6 @@ 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, @@ -313,8 +299,6 @@ SELECT due_date, status, type, - snapshot_id, - source_id, created_at, updated_at FROM @@ -354,15 +338,13 @@ func (os *Obligations) CountByControlID( WITH obls AS ( SELECT o.id, - o.tenant_id, - o.snapshot_id + o.tenant_id FROM obligations o INNER JOIN controls_obligations co ON o.id = co.obligation_id WHERE co.control_id = @control_id - AND o.snapshot_id IS NULL ) SELECT COUNT(id) @@ -409,8 +391,6 @@ WITH obls AS ( o.due_date, o.status, o.type, - o.snapshot_id, - o.source_id, o.created_at, o.updated_at, o.tenant_id @@ -420,7 +400,6 @@ 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, @@ -435,8 +414,6 @@ SELECT due_date, status, type, - snapshot_id, - source_id, created_at, updated_at FROM @@ -485,8 +462,6 @@ INSERT INTO obligations ( due_date, status, type, - snapshot_id, - source_id, created_at, updated_at ) VALUES ( @@ -503,8 +478,6 @@ INSERT INTO obligations ( @due_date, @status, @type, - @snapshot_id, - @source_id, @created_at, @updated_at ) @@ -524,8 +497,6 @@ INSERT INTO obligations ( "due_date": o.DueDate, "status": o.Status, "type": o.Type, - "snapshot_id": o.SnapshotID, - "source_id": o.SourceID, "created_at": o.CreatedAt, "updated_at": o.UpdatedAt, } @@ -559,7 +530,6 @@ UPDATE obligations SET WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -598,7 +568,6 @@ DELETE FROM obligations WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -634,8 +603,6 @@ SELECT due_date, status, type, - snapshot_id, - source_id, created_at, updated_at FROM @@ -643,7 +610,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ORDER BY created_at ASC ` diff --git a/pkg/coredata/processing_activities.go b/pkg/coredata/processing_activities.go index bf0bcca6d..8b0e5c419 100644 --- a/pkg/coredata/processing_activities.go +++ b/pkg/coredata/processing_activities.go @@ -137,8 +137,6 @@ WHERE type ( ProcessingActivity struct { ID gid.GID `db:"id"` - SnapshotID *gid.GID `db:"snapshot_id"` - SourceID *gid.GID `db:"source_id"` OrganizationID gid.GID `db:"organization_id"` Name string `db:"name"` Purpose *string `db:"purpose"` @@ -200,8 +198,6 @@ func (p *ProcessingActivity) LoadByID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, name, purpose, @@ -266,7 +262,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -299,8 +294,6 @@ func (p *ProcessingActivities) LoadByIDs( q := ` SELECT id, - snapshot_id, - source_id, organization_id, name, purpose, @@ -365,8 +358,6 @@ func (p *ProcessingActivities) LoadByOrganizationID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, name, purpose, @@ -394,7 +385,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -428,8 +418,6 @@ func (p *ProcessingActivities) LoadAllByOrganizationID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, name, purpose, @@ -457,7 +445,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ORDER BY created_at DESC ` @@ -490,8 +477,6 @@ func (p *ProcessingActivity) Insert( INSERT INTO processing_activities ( id, tenant_id, - snapshot_id, - source_id, organization_id, name, purpose, @@ -517,8 +502,6 @@ INSERT INTO processing_activities ( ) VALUES ( @id, @tenant_id, - @snapshot_id, - @source_id, @organization_id, @name, @purpose, @@ -547,8 +530,6 @@ INSERT INTO processing_activities ( args := pgx.StrictNamedArgs{ "id": p.ID, "tenant_id": scope.GetTenantID(), - "snapshot_id": p.SnapshotID, - "source_id": p.SourceID, "organization_id": p.OrganizationID, "name": p.Name, "purpose": p.Purpose, @@ -612,7 +593,6 @@ SET WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -660,7 +640,6 @@ DELETE FROM processing_activities WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/processing_activity_third_party.go b/pkg/coredata/processing_activity_third_party.go index 23c2943ee..8b0aad4c2 100644 --- a/pkg/coredata/processing_activity_third_party.go +++ b/pkg/coredata/processing_activity_third_party.go @@ -29,7 +29,6 @@ type ( ProcessingActivityID gid.GID `db:"processing_activity_id"` ThirdPartyID gid.GID `db:"third_party_id"` TenantID gid.TenantID `db:"tenant_id"` - SnapshotID *gid.GID `db:"snapshot_id"` CreatedAt time.Time `db:"created_at"` } diff --git a/pkg/coredata/risk.go b/pkg/coredata/risk.go index 12c79617a..baec736db 100644 --- a/pkg/coredata/risk.go +++ b/pkg/coredata/risk.go @@ -214,7 +214,6 @@ WITH rsks AS ( risks_measures rm ON r.id = rm.risk_id WHERE rm.measure_id = @measure_id - AND r.snapshot_id IS NULL ) SELECT COUNT(id) @@ -277,7 +276,6 @@ WITH rsks AS ( iam_membership_profiles p ON r.owner_profile_id = p.id WHERE rm.measure_id = @measure_id - AND r.snapshot_id IS NULL ) SELECT id, @@ -338,7 +336,6 @@ SELECT FROM risks WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) @@ -393,7 +390,6 @@ WITH rsks AS ( iam_membership_profiles p ON r.owner_profile_id = p.id WHERE r.organization_id = @organization_id - AND r.snapshot_id IS NULL ) SELECT id, @@ -470,7 +466,6 @@ FROM risks r WHERE %s AND r.organization_id = @organization_id - AND r.snapshot_id IS NULL ORDER BY r.name ASC, r.id ASC ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -649,7 +644,6 @@ SET updated_at = @updated_at WHERE %s AND id = @risk_id - AND snapshot_id IS NULL RETURNING inherent_risk_score, residual_risk_score ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -688,7 +682,7 @@ func (r *Risk) Delete( riskID gid.GID, ) error { q := ` -DELETE FROM risks WHERE %s AND id = @id AND snapshot_id IS NULL +DELETE FROM risks WHERE %s AND id = @id ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -718,7 +712,6 @@ WITH rsks AS ( risks_documents rd ON r.id = rd.risk_id WHERE rd.document_id = @document_id - AND r.snapshot_id IS NULL ) SELECT COUNT(id) diff --git a/pkg/coredata/statement_of_applicability.go b/pkg/coredata/statement_of_applicability.go index ccb51dce6..d017fbcd1 100644 --- a/pkg/coredata/statement_of_applicability.go +++ b/pkg/coredata/statement_of_applicability.go @@ -85,7 +85,6 @@ FROM WHERE %s AND id = @statement_of_applicability_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -132,7 +131,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) @@ -169,7 +167,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -252,7 +249,6 @@ SET WHERE %s AND id = @statement_of_applicability_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -293,7 +289,6 @@ DELETE FROM statements_of_applicability WHERE %s AND id = @statement_of_applicability_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/third_party.go b/pkg/coredata/third_party.go index 52a83ba64..b86f6dd01 100644 --- a/pkg/coredata/third_party.go +++ b/pkg/coredata/third_party.go @@ -446,7 +446,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -505,7 +504,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ORDER BY name ASC ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -568,7 +566,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s AND %s ` @@ -1160,7 +1157,6 @@ WITH filtered_processing_activities AS ( WHERE pa.tenant_id = @tenant_id AND pa.organization_id = @organization_id - AND pa.snapshot_id IS NULL ), filtered_third_parties AS ( SELECT @@ -1170,7 +1166,6 @@ filtered_third_parties AS ( third_parties v WHERE v.tenant_id = @tenant_id - AND v.snapshot_id IS NULL ) SELECT pav.processing_activity_id, diff --git a/pkg/coredata/third_party_business_associate_agreement.go b/pkg/coredata/third_party_business_associate_agreement.go index 7ff3799f1..fa6363af4 100644 --- a/pkg/coredata/third_party_business_associate_agreement.go +++ b/pkg/coredata/third_party_business_associate_agreement.go @@ -22,7 +22,6 @@ import ( "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" @@ -89,7 +88,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -139,7 +137,6 @@ FROM WHERE %s AND third_party_id = ANY(@third_party_ids) - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -227,7 +224,6 @@ SET WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -299,12 +295,6 @@ ON CONFLICT (organization_id, third_party_id) DO UPDATE SET _, err := conn.Exec(ctx, q, args) if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - if pgErr.Code == "23505" && pgErr.ConstraintName == "third_party_business_associate_agreements_source_id_snapshot_id_key" { - return ErrResourceAlreadyExists - } - } return fmt.Errorf("cannot upsert thirdParty business associate agreement: %w", err) } return nil @@ -322,7 +312,6 @@ FROM WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -347,7 +336,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/third_party_compliance_report.go b/pkg/coredata/third_party_compliance_report.go index 8c6930147..b2307a5e2 100644 --- a/pkg/coredata/third_party_compliance_report.go +++ b/pkg/coredata/third_party_compliance_report.go @@ -91,7 +91,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL AND %s ` @@ -143,7 +142,6 @@ FROM WHERE %s AND third_party_id = ANY(@third_party_ids) - AND snapshot_id IS NULL ORDER BY third_party_id, report_date DESC ` @@ -279,7 +277,6 @@ FROM WHERE %s AND id = @id - AND snapshot_id IS NULL RETURNING report_file_id ` diff --git a/pkg/coredata/third_party_contact.go b/pkg/coredata/third_party_contact.go index ef5694c90..1ecec4aa5 100644 --- a/pkg/coredata/third_party_contact.go +++ b/pkg/coredata/third_party_contact.go @@ -144,7 +144,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) @@ -198,7 +197,6 @@ FROM WHERE %s AND third_party_id = ANY(@third_party_ids) - AND snapshot_id IS NULL ORDER BY third_party_id, full_name ASC ` @@ -299,7 +297,6 @@ SET WHERE %s AND id = @third_party_contact_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -333,7 +330,6 @@ DELETE FROM WHERE %s AND id = @third_party_contact_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/third_party_data_privacy_agreement.go b/pkg/coredata/third_party_data_privacy_agreement.go index a8cdc4297..be4a6aaee 100644 --- a/pkg/coredata/third_party_data_privacy_agreement.go +++ b/pkg/coredata/third_party_data_privacy_agreement.go @@ -22,7 +22,6 @@ import ( "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" @@ -89,7 +88,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL LIMIT 1; ` @@ -139,7 +137,6 @@ FROM WHERE %s AND third_party_id = ANY(@third_party_ids) - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -227,7 +224,6 @@ SET WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -299,12 +295,6 @@ ON CONFLICT (organization_id, third_party_id) DO UPDATE SET _, err := conn.Exec(ctx, q, args) if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - if pgErr.Code == "23505" && pgErr.ConstraintName == "third_party_data_privacy_agreements_source_id_snapshot_id_key" { - return ErrResourceAlreadyExists - } - } return fmt.Errorf("cannot upsert thirdParty data privacy agreement: %w", err) } return nil @@ -322,7 +312,6 @@ FROM WHERE %s AND id = @id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/third_party_risk_assessment.go b/pkg/coredata/third_party_risk_assessment.go index 819fe3e58..99a2ae193 100644 --- a/pkg/coredata/third_party_risk_assessment.go +++ b/pkg/coredata/third_party_risk_assessment.go @@ -189,7 +189,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL ORDER BY created_at DESC LIMIT 1; @@ -240,7 +239,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL AND %s ` @@ -292,7 +290,6 @@ FROM WHERE %s AND third_party_id = ANY(@third_party_ids) - AND snapshot_id IS NULL ORDER BY third_party_id, created_at DESC ` diff --git a/pkg/coredata/third_party_service.go b/pkg/coredata/third_party_service.go index 362df8086..c441a0985 100644 --- a/pkg/coredata/third_party_service.go +++ b/pkg/coredata/third_party_service.go @@ -135,7 +135,6 @@ FROM WHERE %s AND third_party_id = @third_party_id - AND snapshot_id IS NULL AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) @@ -187,7 +186,6 @@ FROM WHERE %s AND third_party_id = ANY(@third_party_ids) - AND snapshot_id IS NULL ORDER BY third_party_id, name ASC ` @@ -280,7 +278,6 @@ SET WHERE %s AND id = @third_party_service_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -312,7 +309,6 @@ DELETE FROM WHERE %s AND id = @third_party_service_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) diff --git a/pkg/coredata/transfer_impact_assessment.go b/pkg/coredata/transfer_impact_assessment.go index 5fbc54ed5..c7735f504 100644 --- a/pkg/coredata/transfer_impact_assessment.go +++ b/pkg/coredata/transfer_impact_assessment.go @@ -138,8 +138,6 @@ WHERE type ( TransferImpactAssessment struct { ID gid.GID `db:"id"` - SnapshotID *gid.GID `db:"snapshot_id"` - SourceID *gid.GID `db:"source_id"` OrganizationID gid.GID `db:"organization_id"` ProcessingActivityID gid.GID `db:"processing_activity_id"` DataSubjects *string `db:"data_subjects"` @@ -192,7 +190,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -221,8 +218,6 @@ func (tias *TransferImpactAssessments) LoadByOrganizationID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, data_subjects, @@ -237,7 +232,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL AND %s ` @@ -271,8 +265,6 @@ func (tias *TransferImpactAssessments) LoadAllByOrganizationID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, data_subjects, @@ -287,7 +279,6 @@ FROM WHERE %s AND organization_id = @organization_id - AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -319,8 +310,6 @@ func (tia *TransferImpactAssessment) LoadByID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, data_subjects, @@ -370,8 +359,6 @@ func (tia *TransferImpactAssessment) LoadByProcessingActivityID( q := ` SELECT id, - snapshot_id, - source_id, organization_id, processing_activity_id, data_subjects,