diff --git a/pkg/coredata/migrations/20260520T113200Z.sql b/pkg/coredata/migrations/20260520T113200Z.sql new file mode 100644 index 000000000..31b10c428 --- /dev/null +++ b/pkg/coredata/migrations/20260520T113200Z.sql @@ -0,0 +1,86 @@ +-- 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. + +-- Strip empty ProseMirror text nodes from document_versions.content. +-- Empty text nodes are invalid per the ProseMirror schema and cause Tiptap +-- to refuse rendering the document with "Empty text nodes are not allowed". + +CREATE OR REPLACE FUNCTION pg_temp.strip_empty_text_nodes(input jsonb) RETURNS jsonb +LANGUAGE plpgsql IMMUTABLE AS $$ +DECLARE + item jsonb; + new_arr jsonb := '[]'::jsonb; + out_obj jsonb; +BEGIN + IF jsonb_typeof(input) = 'object' THEN + out_obj := input; + IF input ? 'content' AND jsonb_typeof(input->'content') = 'array' THEN + FOR item IN SELECT * FROM jsonb_array_elements(input->'content') LOOP + IF item->>'type' = 'text' + AND (item->>'text' IS NULL OR item->>'text' = '') THEN + CONTINUE; + END IF; + new_arr := new_arr || jsonb_build_array(pg_temp.strip_empty_text_nodes(item)); + END LOOP; + out_obj := jsonb_set(out_obj, '{content}', new_arr); + END IF; + RETURN out_obj; + ELSIF jsonb_typeof(input) = 'array' THEN + FOR item IN SELECT * FROM jsonb_array_elements(input) LOOP + new_arr := new_arr || jsonb_build_array(pg_temp.strip_empty_text_nodes(item)); + END LOOP; + RETURN new_arr; + ELSE + RETURN input; + END IF; +END; +$$; + +DO $$ +DECLARE + r record; + cleaned jsonb; + cleaned_txt text; + scanned integer := 0; + updated integer := 0; + skipped integer := 0; +BEGIN + FOR r IN + SELECT dv.id, dv.content + FROM document_versions dv + JOIN documents d ON d.id = dv.document_id + WHERE d.write_mode = 'GENERATED' + AND (dv.content LIKE '%"text":""%' OR dv.content LIKE '%"text": ""%') + LOOP + scanned := scanned + 1; + BEGIN + cleaned := pg_temp.strip_empty_text_nodes(r.content::jsonb); + EXCEPTION WHEN OTHERS THEN + skipped := skipped + 1; + RAISE NOTICE 'document_versions.id=% skipped: %', r.id, SQLERRM; + CONTINUE; + END; + + cleaned_txt := cleaned::text; + IF cleaned_txt IS DISTINCT FROM r.content THEN + UPDATE document_versions + SET content = cleaned_txt + WHERE id = r.id; + updated := updated + 1; + END IF; + END LOOP; + + RAISE NOTICE 'strip_empty_text_nodes: scanned=% updated=% skipped=%', + scanned, updated, skipped; +END $$; diff --git a/pkg/probo/generated_document_service.go b/pkg/probo/generated_document_service.go index 78b8c6696..08feb0fae 100644 --- a/pkg/probo/generated_document_service.go +++ b/pkg/probo/generated_document_service.go @@ -2702,6 +2702,25 @@ var thirdPartyListTemplate = template.Must( }, "printf": fmt.Sprintf, "add": func(a, b int) int { return a + b }, + "default": func(fallback string, v any) string { + switch s := v.(type) { + case nil: + return fallback + case string: + if s == "" { + return fallback + } + + return s + default: + str := fmt.Sprint(v) + if str == "" { + return fallback + } + + return str + } + }, }). ParseFS(Templates, "templates/third_party_list.json.tmpl"), ) diff --git a/pkg/probo/templates/third_party_list.json.tmpl b/pkg/probo/templates/third_party_list.json.tmpl index 6c02f5ec8..7cba5bf55 100644 --- a/pkg/probo/templates/third_party_list.json.tmpl +++ b/pkg/probo/templates/third_party_list.json.tmpl @@ -30,42 +30,42 @@ "type": "paragraph", "content": [ { "type": "text", "text": "Legal Name: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.LegalName}} } + { "type": "text", "text": {{json (default "—" $r.LegalName)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Description: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.Description}} } + { "type": "text", "text": {{json (default "—" $r.Description)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Category: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.Category}} } + { "type": "text", "text": {{json (default "—" $r.Category)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Headquarter Address: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.HeadquarterAddress}} } + { "type": "text", "text": {{json (default "—" $r.HeadquarterAddress)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Countries: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.Countries}} } + { "type": "text", "text": {{json (default "—" $r.Countries)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Certifications: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.Certifications}} } + { "type": "text", "text": {{json (default "—" $r.Certifications)}} } ] }, { @@ -77,70 +77,70 @@ "type": "paragraph", "content": [ { "type": "text", "text": "Website: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.WebsiteURL}} } + { "type": "text", "text": {{json (default "—" $r.WebsiteURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Privacy Policy: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.PrivacyPolicyURL}} } + { "type": "text", "text": {{json (default "—" $r.PrivacyPolicyURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Service Level Agreement: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.ServiceLevelAgreementURL}} } + { "type": "text", "text": {{json (default "—" $r.ServiceLevelAgreementURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Data Processing Agreement: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.DataProcessingAgreementURL}} } + { "type": "text", "text": {{json (default "—" $r.DataProcessingAgreementURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Business Associate Agreement: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.BusinessAssociateAgreementURL}} } + { "type": "text", "text": {{json (default "—" $r.BusinessAssociateAgreementURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Subprocessors List: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.SubprocessorsListURL}} } + { "type": "text", "text": {{json (default "—" $r.SubprocessorsListURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Status Page: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.StatusPageURL}} } + { "type": "text", "text": {{json (default "—" $r.StatusPageURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Terms of Service: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.TermsOfServiceURL}} } + { "type": "text", "text": {{json (default "—" $r.TermsOfServiceURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Security Page: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.SecurityPageURL}} } + { "type": "text", "text": {{json (default "—" $r.SecurityPageURL)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Trust Page: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.TrustPageURL}} } + { "type": "text", "text": {{json (default "—" $r.TrustPageURL)}} } ] }, { @@ -152,14 +152,14 @@ "type": "paragraph", "content": [ { "type": "text", "text": "Business Owner: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.BusinessOwner}} } + { "type": "text", "text": {{json (default "—" $r.BusinessOwner)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Security Owner: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json $r.SecurityOwner}} } + { "type": "text", "text": {{json (default "—" $r.SecurityOwner)}} } ] }, { @@ -171,7 +171,7 @@ "type": "paragraph", "content": [ { "type": "text", "text": {{json (printf "%s — " .Name)}}, "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json .Description}} } + { "type": "text", "text": {{json (default "—" .Description)}} } ] }{{end}}{{else}}, { @@ -203,20 +203,20 @@ "type": "paragraph", "content": [ { "type": "text", "text": "Assessed on: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json .AssessedAt}} }, + { "type": "text", "text": {{json (default "—" .AssessedAt)}} }, { "type": "text", "text": " · Expires on: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json .ExpiresAt}} }, + { "type": "text", "text": {{json (default "—" .ExpiresAt)}} }, { "type": "text", "text": " · Data Sensitivity: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json .DataSensitivity}} }, + { "type": "text", "text": {{json (default "—" .DataSensitivity)}} }, { "type": "text", "text": " · Business Impact: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json .BusinessImpact}} } + { "type": "text", "text": {{json (default "—" .BusinessImpact)}} } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Notes: ", "marks": [{ "type": "bold" }] }, - { "type": "text", "text": {{json .Notes}} } + { "type": "text", "text": {{json (default "—" .Notes)}} } ] }{{end}}{{else}}, {