From cd33f380f78c73bd193305f99078ee994126f516 Mon Sep 17 00:00:00 2001 From: kadp89 Date: Tue, 14 Oct 2025 03:30:25 -0700 Subject: [PATCH 01/15] Add SummitEventsLinkHandler and trigger initial draft --- .../classes/SummitEventsLinkHandler.cls | 291 ++++++++++++++++++ .../SummitEventsLinkHandler.cls-meta.xml | 5 + .../SummitEventsSummitEventTrigger.trigger | 13 + ...tEventsSummitEventTrigger.trigger-meta.xml | 5 + 4 files changed, 314 insertions(+) create mode 100644 force-app/main/default/classes/SummitEventsLinkHandler.cls create mode 100644 force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml create mode 100644 force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger create mode 100644 force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls new file mode 100644 index 00000000..25a2160b --- /dev/null +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -0,0 +1,291 @@ +public class SummitEventsLinkHandler { + + // Inelegant, but schema introspection can't differentiate RTF from regular text fields + private static final Set RICHTEXT_FIELDS = new Set{ + 'Event_Full_Text__c', 'Event_Confirmation_Description__c', + 'Event_Additional_Questions_Description__c', + 'Event_Appointment_Description__c', 'Event_Footer__c', + 'Event_Submit_Description__c', 'Event_Cancel_Review_Description__c', + 'Event_Payment_Due_Description__c', 'Event_Payment_Received_Description__c', + 'Event_Description__c', 'Event_Donation_Description__c', + 'Event_Guest_Registration_Description__c' + }; + + public static void run(Map newmap) { + List idList = newmap.keySet(); + newmap = null; + replaceLinks(idList); + } + + public static void replaceLinks(List newlist) { + + List sewList = new List(); + List seList = populateEvents(newlist); + + /** + * Step 1: filter for Summit Events with RTF links and wrap them + */ + + for(Summit_Events__c se : seList) { + SummitEventWrapper sew = new SummitEventWrapper(); + for (String s : RICHTEXT_FIELDS) { + String fieldVal = se?.get(s); + if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); + + for (String field : sew.fieldsWithLinks) { + String rtf = (String) sew.event.get(field); + List itemList = new List(); + + Integer searchStart = 0; + while (true) { + Integer imgStart = rtf.indexOf(' urlToCVMap = new Map(); + Map urlToCDMap = new Map(); + Set urlSet = getURLs(sewList); + Set cvList = [ + SELECT Id, + ContentUrl, + Name + FROM ContentVersion + WHERE ContentUrl IN :urlSet + ]; + Set cdList = [ + SELECT Id, + DistributionPublicUrl, + Name, + ContentVersionId + FROM ContentDistribution + WHERE DistributionPublicUrl IN :urlSet + ]; + + for(ContentVersion cv : cvList) + urlToCVMap.put(cv.ContentUrl,cv); + for(ContentDistribution cd : cdList) + urlToCDMap.put(cd.DistributionPublicUrl,cd); + + + /** + * Step 4: create ContentDistributions for every URL found which: + * -has a CV + * -has no CD + */ + + //List cvsWithoutCds = new List(); + List cdInsertList = new List(); + + for(SummitEventWrapper sew : sewList) { + for(String s : sew.fieldsToLinkMap.keySet()) { + for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(s)) { + ContentVersion cv = urlToCVMap.get(sfi.url); + ContentDistribution cd = urlToCDMap.get(sfi.url); + if(cd == null && cv != null) { + ContentDistribution newCd = new ContentDistribution(); + newCd.name = cv.name + '__SHARE'; + newCd.ContentVersionId = cv.Id; + cdInsertList.add(newCd); + //cvsWithoutCds.add(cv); + sfi.cd = newCd; + sfi.cv = cv; + } + } + } + } + + try { + insert cdInsertList; + } catch (Exception e) { + System.debug(e.getMessage()); + } + + + /** + * Step 5: replace old URLs with new public links attached to a CD + * and update SummitEvents + */ + + List refreshed = [ + SELECT Id,Name,DistributionPublicUrl + FROM ContentDistribution + WHERE Id IN :cdInsertList + ]; + Map cdIdMap = new Map(refreshed); + + /** + * What follows is purposeful DML in a loop. Given the problem space + * and realistic use expectations, orgs are unlikely to be pushing + * large amounts of system-generated Summit Events records. We must instead + * deal with potentially massive, heap-busting objects with large RTFs. + * As such, we trade heap safety and near-guaranteed tx success for + * theoretical but unlikely bulk-unsafe DML ops. Alternatives are async, + * which adds potential end-user friction as they must wait for records + * to push to the database when saving via GUI. + */ + for(SummitEventsWrapper sew : sewList) { + Summit_Events__c se = new Summit_Events__c(); + se.Id = sew.event.Id; + for(String field : sew.fieldsWithLinks) { + String rtf = sew.event.get(field); + for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + rtf = rtf.replace(sfi.url, cdIdMap.get(sfi.cd.Id).DistributionPublicUrl); + } + se.put(field,rtf); + } + try { + update se; + } catch (Exception e) { + System.debug(e); + } + } + + + + } + /** + * Helper method to generate a list of SummitEvents from IDs + * passed in the trigger context. This is necessary because + * heap size is a significant constaint here in theory. We + * deal with potentially massive objects. IDs are passed in + * so we can rig the garbage collector in our favor -- we want + * as few explicit references to large objects as we can, but + * since records passed by trigger.new are immutable, we must + * also query a new set. This method accepts those IDs and + * returns, as obliquely as possible, a set of records whose + * references we can hopefully null later if they are unneeded. + */ + private static List populateEvents(List queryIds) { + return Database.query(generateQuery()); + } + + /** + * Helper method to generate query dynamically. As such, for + * updates to the Summit_Events__c object's RTFs, all is required is + * to add or delete members from the global RICHTEXT_FIELDS set above + */ + private static String generateQuery() { + List fieldList = new List(RICHTEXT_FIELDS); + String fullQuery = 'SELECT ' + fieldList.remove(0); + for(Integer i = fieldList.size() - 1; i >= 0; i--) { + fullQuery += ', ' + fieldList.remove(i); + } + fullQuery += ' FROM Summit_Events__c WHERE Id in :queryIds'; + return fullQuery; + } + + /** + * Helper method to generate a set of all URLs found in RTFs + */ + private static Set getURLs(List sewList) { + Set returnSet = new Set(); + for(SummitEventWrapper sew : sewList) { + for(String s : sew.fieldsToLinkMap.keySet()) { + for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(s)) { + returnSet.add(sfi.url); + } + } + } + return returnSet; + } + + // Simple wrapper to aggregate relevant values we'll use + private class SummitEventWrapper { + Summit_Events__c event; + Set fieldsWithLinks; + Map> fieldsToLinkMap; + + private SummitEventWrapper() { + this.fieldsWithLinks = new Set(); + this.fieldsToLinkMap = new Map>(); + } + + private SummitEventWrapper(Summit_Events__c event) { + this.event = event; + this.fieldsWithLinks = new Set(); + this.fieldsToLinkMap = new Map>(); + } + } + + private class SummitFieldItem { + String url; + ContentVersion cv; + ContentDistribution cd; + private SummitFieldItem() {} + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml new file mode 100644 index 00000000..1e7de940 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger new file mode 100644 index 00000000..907fa3e1 --- /dev/null +++ b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger @@ -0,0 +1,13 @@ +trigger SummitEventsSummitEventTrigger on Summit_Events__c (before insert, before update, after insert, after update) { + Summit_Events_Settings__c SummitEventsSettings = Summit_Events_Settings__c.getOrgDefaults(); + if (!SummitEventsSettings.Turn_off_Summit_Events_Trigger__c) { + if (SummitEventsSettings.Opt_Into_Link_Automation__c) { + if (Trigger.isInsert && Trigger.isAfter) { + SummitEventsLinkHandler.run(Trigger.newMap); + } + if (Trigger.isUpdate && Trigger.isAfter) { + SummitEventsLinkHandler.run(Trigger.newMap); + } + } + } +} \ No newline at end of file diff --git a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml new file mode 100644 index 00000000..260ec509 --- /dev/null +++ b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + From b2c6e90ccb5320bc805ea13ef6a0f510febc4d4b Mon Sep 17 00:00:00 2001 From: kadp89 Date: Tue, 14 Oct 2025 04:39:41 -0700 Subject: [PATCH 02/15] Progress on link handler --- .../classes/SummitEventsLinkHandler.cls | 102 +++++++----------- 1 file changed, 40 insertions(+), 62 deletions(-) diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls index 25a2160b..b5febd62 100644 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -12,7 +12,7 @@ public class SummitEventsLinkHandler { }; public static void run(Map newmap) { - List idList = newmap.keySet(); + List idList = new List(newmap.keySet()); newmap = null; replaceLinks(idList); } @@ -29,7 +29,7 @@ public class SummitEventsLinkHandler { for(Summit_Events__c se : seList) { SummitEventWrapper sew = new SummitEventWrapper(); for (String s : RICHTEXT_FIELDS) { - String fieldVal = se?.get(s); + String fieldVal = (String) se?.get(s); if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); @@ -88,12 +55,16 @@ public class SummitEventsLinkHandler { Integer searchStart = 0; while (true) { Integer imgStart = rtf.indexOf(' urlToCVMap = new Map(); - Map urlToCDMap = new Map(); Set urlSet = getURLs(sewList); - Set cvList = [ - SELECT Id, - ContentUrl, - Name - FROM ContentVersion + + List cvList = [ + SELECT Id, + ContentUrl, + Title + FROM ContentVersion WHERE ContentUrl IN :urlSet ]; - Set cdList = [ - SELECT Id, - DistributionPublicUrl, - Name, + + Map urlToCVMap = new Map(); + Set cvIds = new Set(); + + for (ContentVersion cv : cvList) { + urlToCVMap.put(cv.ContentUrl, cv); + cvIds.add(cv.Id); + } + + List cdList = [ + SELECT Id, + DistributionPublicUrl, + Name, ContentVersionId - FROM ContentDistribution - WHERE DistributionPublicUrl IN :urlSet + FROM ContentDistribution + WHERE ContentVersionId IN :cvIds ]; - for(ContentVersion cv : cvList) - urlToCVMap.put(cv.ContentUrl,cv); - for(ContentDistribution cd : cdList) - urlToCDMap.put(cd.DistributionPublicUrl,cd); - + Map cvIdToCDMap = new Map(); + for (ContentDistribution cd : cdList) { + cvIdToCDMap.put(cd.ContentVersionId, cd); + } /** * Step 4: create ContentDistributions for every URL found which: @@ -155,10 +133,10 @@ public class SummitEventsLinkHandler { for(String s : sew.fieldsToLinkMap.keySet()) { for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(s)) { ContentVersion cv = urlToCVMap.get(sfi.url); - ContentDistribution cd = urlToCDMap.get(sfi.url); + ContentDistribution cd = cvIdToCDMap.get(cv.Id); if(cd == null && cv != null) { ContentDistribution newCd = new ContentDistribution(); - newCd.name = cv.name + '__SHARE'; + newCd.name = cv.Title + '__SHARE'; newCd.ContentVersionId = cv.Id; cdInsertList.add(newCd); //cvsWithoutCds.add(cv); @@ -182,7 +160,7 @@ public class SummitEventsLinkHandler { */ List refreshed = [ - SELECT Id,Name,DistributionPublicUrl + SELECT Id, Name, DistributionPublicUrl FROM ContentDistribution WHERE Id IN :cdInsertList ]; @@ -198,11 +176,11 @@ public class SummitEventsLinkHandler { * which adds potential end-user friction as they must wait for records * to push to the database when saving via GUI. */ - for(SummitEventsWrapper sew : sewList) { + for(SummitEventWrapper sew : sewList) { Summit_Events__c se = new Summit_Events__c(); se.Id = sew.event.Id; for(String field : sew.fieldsWithLinks) { - String rtf = sew.event.get(field); + String rtf = (String) sew.event.get(field); for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { rtf = rtf.replace(sfi.url, cdIdMap.get(sfi.cd.Id).DistributionPublicUrl); } From 9fdcc5685680578b13e764d5c2d0452ac7183b07 Mon Sep 17 00:00:00 2001 From: kadp89 Date: Tue, 14 Oct 2025 06:37:51 -0700 Subject: [PATCH 03/15] Progress on link handler --- .../classes/SummitEventsLinkHandler.cls | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls index b5febd62..d118925b 100644 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -11,6 +11,9 @@ public class SummitEventsLinkHandler { 'Event_Guest_Registration_Description__c' }; + private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); + + public static void run(Map newmap) { List idList = new List(newmap.keySet()); newmap = null; @@ -29,9 +32,10 @@ public class SummitEventsLinkHandler { for(Summit_Events__c se : seList) { SummitEventWrapper sew = new SummitEventWrapper(); for (String s : RICHTEXT_FIELDS) { - String fieldVal = (String) se?.get(s); + String fieldName = namespace + s; + String fieldVal = (String) se?.get(fieldName); if(!String.isBlank(fieldVal) && fieldVal.contains(' fieldList = new List(RICHTEXT_FIELDS); - String fullQuery = 'SELECT ' + fieldList.remove(0); - for(Integer i = fieldList.size() - 1; i >= 0; i--) { - fullQuery += ', ' + fieldList.remove(i); + List fields = new List(); + for(String f : RICHTEXT_FIELDS) { + fields.add(namespace + f); } - fullQuery += ' FROM Summit_Events__c WHERE Id in :queryIds'; - return fullQuery; - } + String fieldList = String.join(fields, ', '); + return 'SELECT ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :queryIds'; + } /** * Helper method to generate a set of all URLs found in RTFs From e41539ae68f50e10a343c21aee74ecbec0f22362 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Sun, 19 Oct 2025 02:15:31 -0700 Subject: [PATCH 04/15] Add fields to global config object --- .../fields/Opt_Into_Link_Automation__c.field-meta.xml | 11 +++++++++++ .../Turn_off_Summit_Events_Trigger__c.field-meta.xml | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml create mode 100644 force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml diff --git a/force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml new file mode 100644 index 00000000..d3165e8b --- /dev/null +++ b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Opt_Into_Link_Automation__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Opt_Into_Link_Automation__c + false + Opt into automatic creation of public links backed by ContentVersion files found in Summit Events RTFs. + false + Opt into automatic creation of public links backed by ContentVersion files found in Summit Events RTFs. + + false + Checkbox + diff --git a/force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml new file mode 100644 index 00000000..da9e5c72 --- /dev/null +++ b/force-app/main/default/objects/Summit_Events_Settings__c/fields/Turn_off_Summit_Events_Trigger__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Turn_off_Summit_Events_Trigger__c + false + Turns off Summit Event App's Summit Events trigger. + false + Turns off Summit Event App's Summit Events trigger. + + false + Checkbox + From b0e64c55e9191017623b78414263b91058d8db56 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Sun, 19 Oct 2025 05:19:54 -0700 Subject: [PATCH 05/15] add test class; work on debugging --- .../classes/SummitEventsLinkHandler.cls | 42 +++++++++++++++++-- .../classes/SummitEventsLinkHandler_TEST.cls | 33 +++++++++++++++ .../SummitEventsLinkHandler_TEST.cls-meta.xml | 5 +++ 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls create mode 100644 force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls index d118925b..75a68279 100644 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -7,8 +7,8 @@ public class SummitEventsLinkHandler { 'Event_Appointment_Description__c', 'Event_Footer__c', 'Event_Submit_Description__c', 'Event_Cancel_Review_Description__c', 'Event_Payment_Due_Description__c', 'Event_Payment_Received_Description__c', - 'Event_Description__c', 'Event_Donation_Description__c', - 'Event_Guest_Registration_Description__c' + 'Event_description__c', 'Donation_Description__c', + 'Guest_Registration_Description__c' }; private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); @@ -87,6 +87,38 @@ public class SummitEventsLinkHandler { sew.fieldsToLinkMap = tempMap; } + + /*for(SummitEventWrapper sew : sewList) { + Map> tempMap = new Map>(); + for(String s : sew.fieldsWithLinks) { + List itemList = new List(); + String rtf = (String) sew.event.get(s); + String url; + ** + * What follows is dirty and complex string surgery with + * oven mitts; could potentially be refactored to use Patterns + * and Matchers, but native Regex tools are insufficient + * on their own for index-based logic and Regex has significant + * overhead in time complexity. + * + for (Integer i = rtf.indexOf(' cdIdMap = new Map(refreshed); /** diff --git a/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls b/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls new file mode 100644 index 00000000..995c2feb --- /dev/null +++ b/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls @@ -0,0 +1,33 @@ +@IsTest(SeeAllData=true) +private class SummitEventsLinkHandler_TEST { + + @IsTest(SeeAllData=true) + static void basicRun() { + + String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); + + ContentDistribution realCD = [SELECT Id,ContentVersionId FROM ContentDistribution LIMIT 1]; + Id realCVId = realCD.ContentVersionId; + ContentVersion realCV = [SELECT Id,Title,PathOnClient,VersionData,IsMajorVersion,ContentUrl FROM ContentVersion WHERE Id = :realCVId]; + delete realCD; // god help us + + + // Build a fake Summit Event record with an tag in an RTF field + Summit_Events__c se = new Summit_Events__c(Name = 'Test Summit Event'); + se.put(namespace + 'Event_Full_Text__c', ''); + system.debug(realCV.ContentUrl); + insert se; + + // Minimal trigger-style map to simulate trigger.newMap + Map newMap = new Map{ se.Id => se }; + + // Call the handler + Test.startTest(); + SummitEventsLinkHandler.run(newMap); + Test.stopTest(); + + // Basic sanity query to ensure record update didn’t throw + Summit_Events__c updated = [SELECT Id, Event_Full_Text__c FROM Summit_Events__c WHERE Id = :se.Id]; + System.assertNotEquals(null, updated); + } +} diff --git a/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml b/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active + From 1a4ff40a48d8a88c191267d978b160b23ed4ae7a Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Mon, 20 Oct 2025 04:59:00 -0700 Subject: [PATCH 06/15] beginning major refactor --- .../classes/SummitEventsLinkHandler.cls | 47 ++++++++++++---- .../classes/SummitEventsRtfLinkPipeline.cls | 55 +++++++++++++++++++ .../SummitEventsRtfLinkPipeline.cls-meta.xml | 5 ++ 3 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls create mode 100644 force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls index 75a68279..afe6779e 100644 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -70,12 +70,37 @@ public class SummitEventsLinkHandler { } String url = rtf.substring(urlStart, urlEnd); + Id cvId; + + Integer pathIdx = url.indexOf('/sfc/servlet.shepherd/version/download/'); + if (pathIdx != -1) { + Integer idStart = pathIdx + '/sfc/servlet.shepherd/version/download/'.length(); + + Integer i = idStart; + while (i < url.length()) { + String ch = url.substring(i, i + 1); + if ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.indexOf(ch) == -1) { + break; + } + i++; + } + + String possibleId = url.substring(idStart, i); + + if (possibleId.startsWith('068') && (possibleId.length() == 15 || possibleId.length() == 18)) { + try { + cvId = (Id) possibleId; + } catch (Exception e) { + System.debug(e.getMessage()); + } + } + } SummitFieldItem sfi = new SummitFieldItem(); sfi.url = url; + sfi.cvId = cvId; itemList.add(sfi); - // advance search past this tag searchStart = urlEnd + 1; } @@ -124,14 +149,14 @@ public class SummitEventsLinkHandler { * then set up some data we'll need */ - Set urlSet = getURLs(sewList); + Set idSet = getIds(sewList); List cvList = [ SELECT Id, ContentUrl, Title FROM ContentVersion - WHERE ContentUrl IN :urlSet + WHERE ContentUrl IN :idSet ]; Map urlToCVMap = new Map(); @@ -217,9 +242,13 @@ public class SummitEventsLinkHandler { for(SummitEventWrapper sew : sewList) { Summit_Events__c se = new Summit_Events__c(); se.Id = sew.event.Id; + System.debug('step 5: se.Id =' + se.Id); for(String field : sew.fieldsWithLinks) { String rtf = (String) sew.event.get(field); + System.debug('step 5: rtf =' + rtf); for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + System.debug('step 5: sfi.url =' + sfi.url); + rtf = rtf.replace(sfi.url, cdIdMap.get(sfi.cd.Id).DistributionPublicUrl); } se.put(field,rtf); @@ -250,11 +279,6 @@ public class SummitEventsLinkHandler { return Database.query(generateQuery()); } - /** - * Helper method to generate query dynamically. As such, for - * updates to the Summit_Events__c object's RTFs, all is required is - * to add or delete members from the global RICHTEXT_FIELDS set above - */ /** * Helper method to generate query dynamically. As such, for * updates to the Summit_Events__c object's RTFs, all is required is @@ -272,12 +296,12 @@ public class SummitEventsLinkHandler { /** * Helper method to generate a set of all URLs found in RTFs */ - private static Set getURLs(List sewList) { - Set returnSet = new Set(); + private static Set getIds(List sewList) { + Set returnSet = new Set(); for(SummitEventWrapper sew : sewList) { for(String s : sew.fieldsToLinkMap.keySet()) { for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(s)) { - returnSet.add(sfi.url); + returnSet.add(sfi.cvId); } } } @@ -304,6 +328,7 @@ public class SummitEventsLinkHandler { private class SummitFieldItem { String url; + Id cvId; ContentVersion cv; ContentDistribution cd; private SummitFieldItem() {} diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls new file mode 100644 index 00000000..7e3c2dbf --- /dev/null +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -0,0 +1,55 @@ +public with sharing class SummitEventsRtfLinkPipeline { + + // Inelegant, but schema introspection can't differentiate RTF from regular text fields + private static final Set RICHTEXT_FIELDS = new Set{ + 'Event_Full_Text__c', 'Event_Confirmation_Description__c', + 'Event_Additional_Questions_Description__c', + 'Event_Appointment_Description__c', 'Event_Footer__c', + 'Event_Submit_Description__c', 'Event_Cancel_Review_Description__c', + 'Event_Payment_Due_Description__c', 'Event_Payment_Received_Description__c', + 'Event_description__c', 'Donation_Description__c', + 'Guest_Registration_Description__c' + }; + + private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); + + Map cvIdToCDMap; + Map cdIdMap; + private List sewList; + private List idList; + List refreshed; + private String executionStage; + + public SummitEventsRtfLinkPipeline(Map newmap) { + this.idList = new List(newmap.keySet()); + newmap = null; + } + + public void run() { + try { + filter(); + scan(); + + } + } + + // Simple wrapper to aggregate relevant values we'll use + private class SummitEventWrapper { + Summit_Events__c event; + Set fieldsWithLinks; + Map> fieldsToLinkMap; + + private SummitEventWrapper() { + this.fieldsWithLinks = new Set(); + this.fieldsToLinkMap = new Map>(); + } + } + + private class SummitFieldItem { + String url; + Id cvId; + ContentVersion cv; + ContentDistribution cd; + private SummitFieldItem() {} + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active + From ace90457d0cb1e374452ddece1affe2accfab6f3 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Mon, 20 Oct 2025 06:46:11 -0700 Subject: [PATCH 07/15] scaffold RtfLinkPipeline structure; implement filter() and scan(); stub TODO fns --- .../classes/SummitEventsRtfLinkPipeline.cls | 120 +++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index 7e3c2dbf..242a5c7d 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -26,13 +26,131 @@ public with sharing class SummitEventsRtfLinkPipeline { } public void run() { + this.executionStage = 'run'; try { filter(); scan(); - + prepare(); + push(); + } catch(Exception e) { + System.debug('Pipeline failed at stage ' + executionStage + ': ' + e.getMessage()); } } + private void filter() { + + if(idList.isEmpty()) { + if(!this.executionStage.startsWith('*')) + this.executionStage = '***short circuit from ' + this.executionStage + '***'; + return; + } + + this.executionStage = 'filter'; + this.sewList = new List(); + + List seList = new List(); + List fields = new List(); + String query = ''; + + for(String f : RICHTEXT_FIELDS) { + fields.add(namespace + f); + } + + String fieldList = String.join(fields, ', '); + query = 'SELECT ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :this.idList'; + seList = Database.query(query); + + for(Summit_Events__c se : seList) { + SummitEventWrapper sew = new SummitEventWrapper(); + for (String s : RICHTEXT_FIELDS) { + String fieldName = namespace + s; + String fieldVal = (String) se?.get(fieldName); + if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); + + for(String field : sew.fieldsWithLinks) { + String rtf = (String) sew.event.get(field); + List itemList = new List(); + + Integer searchStart = 0; + while (true) { + Integer imgStart = rtf.indexOf(' Date: Tue, 21 Oct 2025 05:28:24 -0700 Subject: [PATCH 08/15] implemented prepare(), create(), and push() --- .../classes/SummitEventsLinkHandler.cls | 337 +----------------- .../classes/SummitEventsRtfLinkPipeline.cls | 111 ++++-- 2 files changed, 91 insertions(+), 357 deletions(-) diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls index afe6779e..d3119bcd 100644 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -1,336 +1 @@ -public class SummitEventsLinkHandler { - - // Inelegant, but schema introspection can't differentiate RTF from regular text fields - private static final Set RICHTEXT_FIELDS = new Set{ - 'Event_Full_Text__c', 'Event_Confirmation_Description__c', - 'Event_Additional_Questions_Description__c', - 'Event_Appointment_Description__c', 'Event_Footer__c', - 'Event_Submit_Description__c', 'Event_Cancel_Review_Description__c', - 'Event_Payment_Due_Description__c', 'Event_Payment_Received_Description__c', - 'Event_description__c', 'Donation_Description__c', - 'Guest_Registration_Description__c' - }; - - private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); - - - public static void run(Map newmap) { - List idList = new List(newmap.keySet()); - newmap = null; - replaceLinks(idList); - } - - public static void replaceLinks(List newlist) { - - List sewList = new List(); - List seList = populateEvents(newlist); - - /** - * Step 1: filter for Summit Events with RTF links and wrap them - */ - - for(Summit_Events__c se : seList) { - SummitEventWrapper sew = new SummitEventWrapper(); - for (String s : RICHTEXT_FIELDS) { - String fieldName = namespace + s; - String fieldVal = (String) se?.get(fieldName); - if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); - for(String s : sew.fieldsWithLinks) { - List itemList = new List(); - String rtf = (String) sew.event.get(s); - String url; - ** - * What follows is dirty and complex string surgery with - * oven mitts; could potentially be refactored to use Patterns - * and Matchers, but native Regex tools are insufficient - * on their own for index-based logic and Regex has significant - * overhead in time complexity. - * - for (Integer i = rtf.indexOf(' idSet = getIds(sewList); - - List cvList = [ - SELECT Id, - ContentUrl, - Title - FROM ContentVersion - WHERE ContentUrl IN :idSet - ]; - - Map urlToCVMap = new Map(); - Set cvIds = new Set(); - - for (ContentVersion cv : cvList) { - urlToCVMap.put(cv.ContentUrl, cv); - cvIds.add(cv.Id); - } - - List cdList = [ - SELECT Id, - DistributionPublicUrl, - Name, - ContentVersionId - FROM ContentDistribution - WHERE ContentVersionId IN :cvIds - ]; - - Map cvIdToCDMap = new Map(); - for (ContentDistribution cd : cdList) { - cvIdToCDMap.put(cd.ContentVersionId, cd); - } - - /** - * Step 4: create ContentDistributions for every URL found which: - * -has a CV - * -has no CD - */ - - //List cvsWithoutCds = new List(); - List cdInsertList = new List(); - - for(SummitEventWrapper sew : sewList) { - for(String s : sew.fieldsToLinkMap.keySet()) { - for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(s)) { - ContentVersion cv = urlToCVMap?.get(sfi?.url); - ContentDistribution cd = cvIdToCDMap?.get(cv?.Id); - if(cd == null && cv != null) { - ContentDistribution newCd = new ContentDistribution(); - newCd.name = cv.Title + '__SHARE'; - newCd.ContentVersionId = cv.Id; - cdInsertList.add(newCd); - //cvsWithoutCds.add(cv); - sfi.cd = newCd; - sfi.cv = cv; - } - } - } - } - - try { - insert cdInsertList; - } catch (Exception e) { - System.debug(e.getMessage()); - } - - - /** - * Step 5: replace old URLs with new public links attached to a CD - * and update SummitEvents - */ - - List refreshed = [ - SELECT Id, Name, DistributionPublicUrl - FROM ContentDistribution - WHERE Id IN :cdInsertList - ]; - System.Assert.areNotEqual(null, refreshed, 'No ContentDistributions were created.'); - System.Assert.areEqual(cdInsertList.size(), refreshed.size(), 'Not all ContentDistributions were created.'); - Map cdIdMap = new Map(refreshed); - - /** - * What follows is purposeful DML in a loop. Given the problem space - * and realistic use expectations, orgs are unlikely to be pushing - * large amounts of system-generated Summit Events records. We must instead - * deal with potentially massive, heap-busting objects with large RTFs. - * As such, we trade heap safety and near-guaranteed tx success for - * theoretical but unlikely bulk-unsafe DML ops. Alternatives are async, - * which adds potential end-user friction as they must wait for records - * to push to the database when saving via GUI. - */ - for(SummitEventWrapper sew : sewList) { - Summit_Events__c se = new Summit_Events__c(); - se.Id = sew.event.Id; - System.debug('step 5: se.Id =' + se.Id); - for(String field : sew.fieldsWithLinks) { - String rtf = (String) sew.event.get(field); - System.debug('step 5: rtf =' + rtf); - for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { - System.debug('step 5: sfi.url =' + sfi.url); - - rtf = rtf.replace(sfi.url, cdIdMap.get(sfi.cd.Id).DistributionPublicUrl); - } - se.put(field,rtf); - } - try { - update se; - } catch (Exception e) { - System.debug(e); - } - } - - - - } - /** - * Helper method to generate a list of SummitEvents from IDs - * passed in the trigger context. This is necessary because - * heap size is a significant constaint here in theory. We - * deal with potentially massive objects. IDs are passed in - * so we can rig the garbage collector in our favor -- we want - * as few explicit references to large objects as we can, but - * since records passed by trigger.new are immutable, we must - * also query a new set. This method accepts those IDs and - * returns, as obliquely as possible, a set of records whose - * references we can hopefully null later if they are unneeded. - */ - private static List populateEvents(List queryIds) { - return Database.query(generateQuery()); - } - - /** - * Helper method to generate query dynamically. As such, for - * updates to the Summit_Events__c object's RTFs, all is required is - * to add or delete members from the global RICHTEXT_FIELDS set above - */ - private static String generateQuery() { - List fields = new List(); - for(String f : RICHTEXT_FIELDS) { - fields.add(namespace + f); - } - String fieldList = String.join(fields, ', '); - return 'SELECT ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :queryIds'; - } - - /** - * Helper method to generate a set of all URLs found in RTFs - */ - private static Set getIds(List sewList) { - Set returnSet = new Set(); - for(SummitEventWrapper sew : sewList) { - for(String s : sew.fieldsToLinkMap.keySet()) { - for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(s)) { - returnSet.add(sfi.cvId); - } - } - } - return returnSet; - } - - // Simple wrapper to aggregate relevant values we'll use - private class SummitEventWrapper { - Summit_Events__c event; - Set fieldsWithLinks; - Map> fieldsToLinkMap; - - private SummitEventWrapper() { - this.fieldsWithLinks = new Set(); - this.fieldsToLinkMap = new Map>(); - } - - private SummitEventWrapper(Summit_Events__c event) { - this.event = event; - this.fieldsWithLinks = new Set(); - this.fieldsToLinkMap = new Map>(); - } - } - - private class SummitFieldItem { - String url; - Id cvId; - ContentVersion cv; - ContentDistribution cd; - private SummitFieldItem() {} - } -} \ No newline at end of file +public class SummitEventsLinkHandler { /* TODO */ } \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index 242a5c7d..2907d5ca 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -13,11 +13,9 @@ public with sharing class SummitEventsRtfLinkPipeline { private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); - Map cvIdToCDMap; - Map cdIdMap; + private Map cvIdToCDMap; private List sewList; private List idList; - List refreshed; private String executionStage; public SummitEventsRtfLinkPipeline(Map newmap) { @@ -31,19 +29,14 @@ public with sharing class SummitEventsRtfLinkPipeline { filter(); scan(); prepare(); + create(); push(); } catch(Exception e) { - System.debug('Pipeline failed at stage ' + executionStage + ': ' + e.getMessage()); + System.debug('Pipeline failed at stage ' + this.executionStage + ': ' + e.getMessage()); } } private void filter() { - - if(idList.isEmpty()) { - if(!this.executionStage.startsWith('*')) - this.executionStage = '***short circuit from ' + this.executionStage + '***'; - return; - } this.executionStage = 'filter'; this.sewList = new List(); @@ -59,6 +52,7 @@ public with sharing class SummitEventsRtfLinkPipeline { String fieldList = String.join(fields, ', '); query = 'SELECT ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :this.idList'; seList = Database.query(query); + // this.idList.clear(); for(Summit_Events__c se : seList) { SummitEventWrapper sew = new SummitEventWrapper(); @@ -71,19 +65,14 @@ public with sharing class SummitEventsRtfLinkPipeline { } if(!sew.fieldsWithLinks.isEmpty()) { sew.event = se; - sewList.add(sew); + this.sewList.add(sew); + // this.idList.add(se.Id); } } } private void scan() { - if(sewList.isEmpty()) { - if(!this.executionStage.startsWith('*')) - this.executionStage = '***short circuit from ' + this.executionStage + '***'; - return; - } - this.executionStage = 'scan'; for(SummitEventWrapper sew : sewList) { @@ -148,8 +137,90 @@ public with sharing class SummitEventsRtfLinkPipeline { } } - private void prepare() {/* TODO */ } - private void push() { /* TODO */ } + private void prepare() { + + this.executionStage = 'prepare'; + + Set cvIdSet = new Set(); + for(SummitEventWrapper sew : sewList) { + for(String field : sew.fieldsToLinkMap.keySet()) { + for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + cvIdSet.add(sfi.cvId); + } + } + } + + List cdList = [ + SELECT Id, + DistributionPublicUrl, + Name, + ContentVersionId + FROM ContentDistribution + WHERE ContentVersionId IN :cvIdSet + ]; + + this.cvIdToCDMap = new Map(); + + for(ContentDistribution cd : cdList) { + this.cvIdToCDMap.put(cd.ContentVersionId, cd); + } + } + + private void create() { + + this.executionStage = 'create'; + + List cdInsertList = new List(); + + for(SummitEventWrapper sew : sewList) { + for(String field : sew.fieldsToLinkMap.keySet()) { + Integer i = 0; + for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + if(!cvIdToCDMap.containsKey(sfi.cvId)) { + ContentDistribution cd = new ContentDistribution(); + cd.ContentVersionId = sfi.cvId; + cd.Name = sew.event.Name + ' - ' + field + i++; + cdInsertList.add(cd); + } + } + } + } + + if(!cdInsertList.isEmpty()) { + insert cdInsertList; + + List refreshed = [ + SELECT Id, + DistributionPublicUrl, + Name, + ContentVersionId + FROM ContentDistribution + WHERE Id IN :cdInsertList + ]; + + for(ContentDistribution cd : refreshed) { + cvIdToCDMap.put(cd.ContentVersionId, cd); + } + } + } + + private void push() { + + this.executionStage = 'push'; + + for(SummitEventWrapper sew : sewList) { + Summit_Events__c se = new Summit_Events__c(); + se.Id = sew.event.Id; + for(String field : sew.fieldsWithLinks) { + String rtf = (String) sew.event.get(field); + for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + rtf = rtf.replace(sfi.url, cvIdToCDMap.get(sfi.cvId).DistributionPublicUrl); + } + se.put(field,rtf); + } + update se; // DML in loop; intentional workaround for potentially enormous record heap size + } + } // Simple wrapper to aggregate relevant values we'll use private class SummitEventWrapper { @@ -166,8 +237,6 @@ public with sharing class SummitEventsRtfLinkPipeline { private class SummitFieldItem { String url; Id cvId; - ContentVersion cv; - ContentDistribution cd; private SummitFieldItem() {} } } \ No newline at end of file From faeda21611a277ac525b595c12a174506ad9bedf Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Sat, 25 Oct 2025 07:02:08 -0700 Subject: [PATCH 09/15] Cleaned up scan() in RTF pipeline and updated algorithm; added rough test class coverage --- .forceignore | 5 +- .gitignore | 5 +- .../classes/SummitEventsRtfLinkPipeline.cls | 123 ++++++++++-------- .../SummitEventsRtfLinkPipeline_TEST.cls | 41 ++++++ ...mitEventsRtfLinkPipeline_TEST.cls-meta.xml | 5 + 5 files changed, 121 insertions(+), 58 deletions(-) create mode 100644 force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls create mode 100644 force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml diff --git a/.forceignore b/.forceignore index eeed72a8..bffd4f14 100644 --- a/.forceignore +++ b/.forceignore @@ -2,4 +2,7 @@ **/*.ts **/tsconfig*.json **/*.tsbuildinfo -**/eslint.config.mjs \ No newline at end of file +**/eslint.config.mjs +**/jsconfig.json + +**/.eslintrc.json diff --git a/.gitignore b/.gitignore index 9de1c41a..c152324d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,7 @@ target/ node_modules/ /.illuminatedCloud/ **/tsconfig*.json -**/*.tsbuildinfo \ No newline at end of file +**/*.tsbuildinfo +.forceignore +package-lock.json +package.json diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index 2907d5ca..6a098cf7 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -19,8 +19,10 @@ public with sharing class SummitEventsRtfLinkPipeline { private String executionStage; public SummitEventsRtfLinkPipeline(Map newmap) { + this.cvIdToCDMap = new Map(); + this.sewList = new List(); this.idList = new List(newmap.keySet()); - newmap = null; + newmap = null; // prayer to GC gods; likely superstition } public void run() { @@ -36,10 +38,13 @@ public with sharing class SummitEventsRtfLinkPipeline { } } + /** + * Cheap first-pass filter to select and wrap Summit_Events__c records + * that have RTF fields with links to ContentVersion records. + */ private void filter() { this.executionStage = 'filter'; - this.sewList = new List(); List seList = new List(); List fields = new List(); @@ -59,7 +64,7 @@ public with sharing class SummitEventsRtfLinkPipeline { for (String s : RICHTEXT_FIELDS) { String fieldName = namespace + s; String fieldVal = (String) se?.get(fieldName); - if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); - - for(String field : sew.fieldsWithLinks) { - String rtf = (String) sew.event.get(field); - List itemList = new List(); - - Integer searchStart = 0; + + for (String field : sew.fieldsWithLinks) { + String html = (String) sew.event.get(field); + List items = new List(); + + Integer pos = 0; while (true) { - Integer imgStart = rtf.indexOf(' + Integer tagStart = html.indexOf('(); - for(ContentDistribution cd : cdList) { this.cvIdToCDMap.put(cd.ContentVersionId, cd); } } + /** + * Create ContentDistribution records for any ContentVersion records + * that are not already linked to a ContentDistribution record. + */ private void create() { this.executionStage = 'create'; @@ -204,6 +211,10 @@ public with sharing class SummitEventsRtfLinkPipeline { } } + /** + * Replace all URLs in the RTF fields with the corresponding ContentDistribution + * record's DistributionPublicUrl, and update the Summit_Events__c record. + */ private void push() { this.executionStage = 'push'; diff --git a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls new file mode 100644 index 00000000..c5de6ed7 --- /dev/null +++ b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls @@ -0,0 +1,41 @@ +@IsTest +public with sharing class SummitEventsRtfLinkPipeline_TEST { + + @TestSetup + static void makeData() { + ContentVersion cv = new ContentVersion( + Title = 'Test', + PathOnClient = 'test.txt', + VersionData = Blob.valueOf('Test.jpg'), + IsMajorVersion = true + ); + insert cv; + } + + @IsTest + static void testLinkReplacement() { + + ContentVersion cv = [SELECT Id FROM ContentVersion LIMIT 1]; + String namespace = SummitEventsNamespace.getNamespace(); + + String rtfUrl = '

test

'; + + Summit_Events__c se = new Summit_Events__c(); + se.put(namespace + 'Event_Full_Text__c', rtfUrl); + insert se; + + Map newMap = new Map{ se.Id => se }; + + Test.startTest(); + SummitEventsRtfLinkPipeline rtfp = new SummitEventsRtfLinkPipeline(newMap); + rtfp.run(); + Test.stopTest(); + + Summit_Events__c updated = [ + SELECT Event_Full_Text__c FROM Summit_Events__c WHERE Id = :se.Id + ]; + System.assert(!updated.Event_Full_Text__c.contains('versionId=068')); + System.assert(updated.Event_Full_Text__c.contains('https://')); + } +} diff --git a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active + From d2e235c21829e9bcb66ff29bc3a1981715fa374f Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Sun, 26 Oct 2025 03:33:19 -0700 Subject: [PATCH 10/15] Finished minimal test class and updated dynamic query in RTF pipeline --- .../default/classes/SummitEventsRtfLinkPipeline.cls | 2 +- .../classes/SummitEventsRtfLinkPipeline_TEST.cls | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index 6a098cf7..f7923463 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -55,7 +55,7 @@ public with sharing class SummitEventsRtfLinkPipeline { } String fieldList = String.join(fields, ', '); - query = 'SELECT ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :this.idList'; + query = 'SELECT Name, ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :idList'; seList = Database.query(query); // this.idList.clear(); diff --git a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls index c5de6ed7..82e61513 100644 --- a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls +++ b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls @@ -19,11 +19,16 @@ public with sharing class SummitEventsRtfLinkPipeline_TEST { String namespace = SummitEventsNamespace.getNamespace(); String rtfUrl = '

test

'; + + cv.Id + '?versionId=' + cv.Id + '" alt="test">

'; Summit_Events__c se = new Summit_Events__c(); se.put(namespace + 'Event_Full_Text__c', rtfUrl); + se.put(namespace + 'Event_Name__c', 'Test Event'); + se.put(namespace + 'Event_Status__c', 'Active'); + se.put(namespace + 'Name', 'Test Event'); insert se; + se = [SELECT Event_Full_Text__c,Event_Name__c,Event_Status__c,Name FROM Summit_Events__c WHERE Id = :se.Id]; + Map newMap = new Map{ se.Id => se }; @@ -33,9 +38,11 @@ public with sharing class SummitEventsRtfLinkPipeline_TEST { Test.stopTest(); Summit_Events__c updated = [ - SELECT Event_Full_Text__c FROM Summit_Events__c WHERE Id = :se.Id + SELECT Event_Full_Text__c,Event_Name__c,Event_Status__c,Name FROM Summit_Events__c WHERE Id = :se.Id ]; + ContentDistribution cd = [SELECT Id, DistributionPublicUrl FROM ContentDistribution WHERE ContentVersionId = :cv.Id LIMIT 1]; System.assert(!updated.Event_Full_Text__c.contains('versionId=068')); System.assert(updated.Event_Full_Text__c.contains('https://')); + System.assert(updated.Event_Full_Text__c.contains(cd.DistributionPublicUrl)); } } From a7b3360324c797fbacb41fc3f2272cf0cb7e4ae6 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Sun, 26 Oct 2025 03:43:52 -0700 Subject: [PATCH 11/15] Cleaned up local files, updated ignore rules --- .gitignore | 1 - .../classes/SummitEventsLinkHandler.cls | 1 - .../SummitEventsLinkHandler.cls-meta.xml | 5 --- .../classes/SummitEventsLinkHandler_TEST.cls | 33 ------------------- .../SummitEventsLinkHandler_TEST.cls-meta.xml | 5 --- 5 files changed, 45 deletions(-) delete mode 100644 force-app/main/default/classes/SummitEventsLinkHandler.cls delete mode 100644 force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml delete mode 100644 force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls delete mode 100644 force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml diff --git a/.gitignore b/.gitignore index c152324d..23990ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,5 @@ node_modules/ /.illuminatedCloud/ **/tsconfig*.json **/*.tsbuildinfo -.forceignore package-lock.json package.json diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls deleted file mode 100644 index d3119bcd..00000000 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ /dev/null @@ -1 +0,0 @@ -public class SummitEventsLinkHandler { /* TODO */ } \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml deleted file mode 100644 index 1e7de940..00000000 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 64.0 - Active - diff --git a/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls b/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls deleted file mode 100644 index 995c2feb..00000000 --- a/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls +++ /dev/null @@ -1,33 +0,0 @@ -@IsTest(SeeAllData=true) -private class SummitEventsLinkHandler_TEST { - - @IsTest(SeeAllData=true) - static void basicRun() { - - String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); - - ContentDistribution realCD = [SELECT Id,ContentVersionId FROM ContentDistribution LIMIT 1]; - Id realCVId = realCD.ContentVersionId; - ContentVersion realCV = [SELECT Id,Title,PathOnClient,VersionData,IsMajorVersion,ContentUrl FROM ContentVersion WHERE Id = :realCVId]; - delete realCD; // god help us - - - // Build a fake Summit Event record with an tag in an RTF field - Summit_Events__c se = new Summit_Events__c(Name = 'Test Summit Event'); - se.put(namespace + 'Event_Full_Text__c', ''); - system.debug(realCV.ContentUrl); - insert se; - - // Minimal trigger-style map to simulate trigger.newMap - Map newMap = new Map{ se.Id => se }; - - // Call the handler - Test.startTest(); - SummitEventsLinkHandler.run(newMap); - Test.stopTest(); - - // Basic sanity query to ensure record update didn’t throw - Summit_Events__c updated = [SELECT Id, Event_Full_Text__c FROM Summit_Events__c WHERE Id = :se.Id]; - System.assertNotEquals(null, updated); - } -} diff --git a/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml b/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml deleted file mode 100644 index 82775b98..00000000 --- a/force-app/test/default/classes/SummitEventsLinkHandler_TEST.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 65.0 - Active - From 53e1cd2ea8da3a71a00ff73ec78c6f02b14943e9 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Mon, 27 Oct 2025 05:58:37 -0700 Subject: [PATCH 12/15] Refactor: use Trigger.new instead of dynamic SOQL Previous version was overly defensive w.r.t. heap limits. After verifying max field lengths on Summit_Events__c (~13k chars), exotic use of DML in loop is unnecessary and best practices can be followed safely. TODO: review push() for null safety and investigate potential duplication issues in create() --- .../classes/SummitEventsRtfLinkPipeline.cls | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index f7923463..ddc1e740 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -1,4 +1,4 @@ -public with sharing class SummitEventsRtfLinkPipeline { +public without sharing class SummitEventsRtfLinkPipeline { // Inelegant, but schema introspection can't differentiate RTF from regular text fields private static final Set RICHTEXT_FIELDS = new Set{ @@ -15,14 +15,13 @@ public with sharing class SummitEventsRtfLinkPipeline { private Map cvIdToCDMap; private List sewList; - private List idList; + private List triggerList; private String executionStage; - public SummitEventsRtfLinkPipeline(Map newmap) { + public SummitEventsRtfLinkPipeline(List triggerList) { this.cvIdToCDMap = new Map(); this.sewList = new List(); - this.idList = new List(newmap.keySet()); - newmap = null; // prayer to GC gods; likely superstition + this.triggerList = triggerList; } public void run() { @@ -46,23 +45,10 @@ public with sharing class SummitEventsRtfLinkPipeline { this.executionStage = 'filter'; - List seList = new List(); - List fields = new List(); - String query = ''; - - for(String f : RICHTEXT_FIELDS) { - fields.add(namespace + f); - } - - String fieldList = String.join(fields, ', '); - query = 'SELECT Name, ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :idList'; - seList = Database.query(query); - // this.idList.clear(); - - for(Summit_Events__c se : seList) { + for(Summit_Events__c se : triggerList) { SummitEventWrapper sew = new SummitEventWrapper(); - for (String s : RICHTEXT_FIELDS) { - String fieldName = namespace + s; + for (String field : RICHTEXT_FIELDS) { + String fieldName = namespace + field; String fieldVal = (String) se?.get(fieldName); if(!String.isBlank(fieldVal) && fieldVal.contains(' updateList = new List(); + for(SummitEventWrapper sew : sewList) { Summit_Events__c se = new Summit_Events__c(); se.Id = sew.event.Id; @@ -229,7 +216,11 @@ public with sharing class SummitEventsRtfLinkPipeline { } se.put(field,rtf); } - update se; // DML in loop; intentional workaround for potentially enormous record heap size + updateList.add(se); + } + + if(!updateList.isEmpty()) { + update updateList; } } From 8a0e308e595fe48cf9b3d2152a409f63c02672a5 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Mon, 27 Oct 2025 06:27:59 -0700 Subject: [PATCH 13/15] Refactor: introduce SummitEventsLinkHandler class and update trigger Adds a dedicated handler for the RTF pipeline. Trigger now delegates to handler and passes in the correct trigger context variable. Lays groundwork for future async processing. --- .../main/default/classes/SummitEventsLinkHandler.cls | 8 ++++++++ .../default/classes/SummitEventsLinkHandler.cls-meta.xml | 5 +++++ .../triggers/SummitEventsSummitEventTrigger.trigger | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 force-app/main/default/classes/SummitEventsLinkHandler.cls create mode 100644 force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls new file mode 100644 index 00000000..8b0b9830 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -0,0 +1,8 @@ +public with sharing class SummitEventsRtfLinkHandler { + public static void run(List newList) { + new SummitEventsRtfLinkPipeline(newList).run(); + } + + // Future consideration: add async logic here + +} \ No newline at end of file diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml new file mode 100644 index 00000000..82775b98 --- /dev/null +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 65.0 + Active + diff --git a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger index 907fa3e1..b4432f44 100644 --- a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger +++ b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger @@ -3,10 +3,10 @@ trigger SummitEventsSummitEventTrigger on Summit_Events__c (before insert, befor if (!SummitEventsSettings.Turn_off_Summit_Events_Trigger__c) { if (SummitEventsSettings.Opt_Into_Link_Automation__c) { if (Trigger.isInsert && Trigger.isAfter) { - SummitEventsLinkHandler.run(Trigger.newMap); + SummitEventsLinkHandler.run(Trigger.new); } if (Trigger.isUpdate && Trigger.isAfter) { - SummitEventsLinkHandler.run(Trigger.newMap); + SummitEventsLinkHandler.run(Trigger.new); } } } From 9bd8ab6f7b0329b07c280d05359fac25ceeead00 Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Tue, 28 Oct 2025 04:16:03 -0700 Subject: [PATCH 14/15] Add duplication safeguard for CD creation; refactor for dynamic SOQL Dynamic SOQL was necessary to ensure the full Summit_Events__c object could be introspected and was reinserted. In support, RTF pipeline constructor refactored to accept Trigger.newMap so Ids can be easily extracted and queried. Test class, trigger framework, and minimal handler all updated accordingly. Ready for review. --- .../classes/SummitEventsLinkHandler.cls | 6 +- .../classes/SummitEventsRtfLinkPipeline.cls | 106 +++++++++++------- .../SummitEventsSummitEventTrigger.trigger | 4 +- .../SummitEventsRtfLinkPipeline_TEST.cls | 2 +- 4 files changed, 70 insertions(+), 48 deletions(-) diff --git a/force-app/main/default/classes/SummitEventsLinkHandler.cls b/force-app/main/default/classes/SummitEventsLinkHandler.cls index 8b0b9830..b1c63cd1 100644 --- a/force-app/main/default/classes/SummitEventsLinkHandler.cls +++ b/force-app/main/default/classes/SummitEventsLinkHandler.cls @@ -1,6 +1,6 @@ -public with sharing class SummitEventsRtfLinkHandler { - public static void run(List newList) { - new SummitEventsRtfLinkPipeline(newList).run(); +public with sharing class SummitEventsLinkHandler { + public static void run(Map newMap) { + new SummitEventsRtfLinkPipeline(newMap).run(); } // Future consideration: add async logic here diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index ddc1e740..1d802d59 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -14,14 +14,14 @@ public without sharing class SummitEventsRtfLinkPipeline { private static final String namespace = SummitEventsNamespace.StrTokenNSPrefix(''); private Map cvIdToCDMap; - private List sewList; - private List triggerList; + private List wrappedEventList; + private List idList; private String executionStage; - public SummitEventsRtfLinkPipeline(List triggerList) { + public SummitEventsRtfLinkPipeline(Map newMap) { this.cvIdToCDMap = new Map(); - this.sewList = new List(); - this.triggerList = triggerList; + this.wrappedEventList = new List(); + this.idList = new List(newMap.keySet()); } public void run() { @@ -41,70 +41,86 @@ public without sharing class SummitEventsRtfLinkPipeline { * Cheap first-pass filter to select and wrap Summit_Events__c records * that have RTF fields with links to ContentVersion records. */ + @TestVisible private void filter() { this.executionStage = 'filter'; - for(Summit_Events__c se : triggerList) { - SummitEventWrapper sew = new SummitEventWrapper(); - for (String field : RICHTEXT_FIELDS) { - String fieldName = namespace + field; - String fieldVal = (String) se?.get(fieldName); + List freshEventList = new List(); + List fields = new List(); + String query = ''; + + for(String field : RICHTEXT_FIELDS) { + fields.add(namespace + field); + } + + String fieldList = String.join(fields, ', '); + query = 'SELECT Name, ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :idList'; + freshEventList = Database.query(query); + + for(Summit_Events__c freshEvent : freshEventList) { + SummitEventWrapper wrappedEvent = new SummitEventWrapper(); + for (String field : fields) { + String fieldVal = (String) freshEvent?.get(field); if(!String.isBlank(fieldVal) && fieldVal.contains('> tempMap = new Map>(); - for (String field : sew.fieldsWithLinks) { - String html = (String) sew.event.get(field); + for (String field : wrappedEvent.fieldsWithLinks) { + String html = (String) wrappedEvent.event.get(field); List items = new List(); Integer pos = 0; while (true) { // Find the next Integer tagStart = html.indexOf(' cvIdSet = new Set(); - for(SummitEventWrapper sew : sewList) { - for(String field : sew.fieldsToLinkMap.keySet()) { - for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + for(SummitEventWrapper wrappedEvent : wrappedEventList) { + for(String field : wrappedEvent.fieldsToLinkMap.keySet()) { + for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) { cvIdSet.add(sfi.cvId); } } @@ -158,21 +175,24 @@ public without sharing class SummitEventsRtfLinkPipeline { * Create ContentDistribution records for any ContentVersion records * that are not already linked to a ContentDistribution record. */ + @TestVisible private void create() { this.executionStage = 'create'; List cdInsertList = new List(); + Set visited = new Set(); - for(SummitEventWrapper sew : sewList) { - for(String field : sew.fieldsToLinkMap.keySet()) { + for(SummitEventWrapper wrappedEvent : wrappedEventList) { + for(String field : wrappedEvent.fieldsToLinkMap.keySet()) { Integer i = 0; - for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { - if(!cvIdToCDMap.containsKey(sfi.cvId)) { + for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) { + if(!cvIdToCDMap.containsKey(sfi.cvId) && !visited.contains(sfi.cvId)) { ContentDistribution cd = new ContentDistribution(); cd.ContentVersionId = sfi.cvId; - cd.Name = sew.event.Name + ' - ' + field + i++; + cd.Name = wrappedEvent.event.Name + ' - ' + field + i++; cdInsertList.add(cd); + visited.add(sfi.cvId); } } } @@ -200,26 +220,28 @@ public without sharing class SummitEventsRtfLinkPipeline { * Replace all URLs in the RTF fields with the corresponding ContentDistribution * record's DistributionPublicUrl, and update the Summit_Events__c record. */ + @TestVisible private void push() { this.executionStage = 'push'; List updateList = new List(); - for(SummitEventWrapper sew : sewList) { - Summit_Events__c se = new Summit_Events__c(); - se.Id = sew.event.Id; - for(String field : sew.fieldsWithLinks) { - String rtf = (String) sew.event.get(field); - for(SummitFieldItem sfi : sew.fieldsToLinkMap.get(field)) { + for(SummitEventWrapper wrappedEvent : wrappedEventList) { + Summit_Events__c tempEvent = new Summit_Events__c(); + tempEvent.Id = wrappedEvent.event.Id; // trigger records are immutable, we must create new records + for(String field : wrappedEvent.fieldsWithLinks) { + String rtf = (String) wrappedEvent.event.get(field); + for(SummitFieldItem sfi : wrappedEvent.fieldsToLinkMap.get(field)) { rtf = rtf.replace(sfi.url, cvIdToCDMap.get(sfi.cvId).DistributionPublicUrl); } - se.put(field,rtf); + tempEvent.put(field,rtf); } - updateList.add(se); + updateList.add(tempEvent); } if(!updateList.isEmpty()) { + this.executionStage = 'push.DML'; update updateList; } } diff --git a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger index b4432f44..907fa3e1 100644 --- a/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger +++ b/force-app/main/default/triggers/SummitEventsSummitEventTrigger.trigger @@ -3,10 +3,10 @@ trigger SummitEventsSummitEventTrigger on Summit_Events__c (before insert, befor if (!SummitEventsSettings.Turn_off_Summit_Events_Trigger__c) { if (SummitEventsSettings.Opt_Into_Link_Automation__c) { if (Trigger.isInsert && Trigger.isAfter) { - SummitEventsLinkHandler.run(Trigger.new); + SummitEventsLinkHandler.run(Trigger.newMap); } if (Trigger.isUpdate && Trigger.isAfter) { - SummitEventsLinkHandler.run(Trigger.new); + SummitEventsLinkHandler.run(Trigger.newMap); } } } diff --git a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls index 82e61513..edfe34bb 100644 --- a/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls +++ b/force-app/test/default/classes/SummitEventsRtfLinkPipeline_TEST.cls @@ -30,7 +30,7 @@ public with sharing class SummitEventsRtfLinkPipeline_TEST { se = [SELECT Event_Full_Text__c,Event_Name__c,Event_Status__c,Name FROM Summit_Events__c WHERE Id = :se.Id]; - Map newMap = new Map{ se.Id => se }; + Map newMap = new Map{ se.Id => se }; Test.startTest(); SummitEventsRtfLinkPipeline rtfp = new SummitEventsRtfLinkPipeline(newMap); From 62853617e906c281f9b8e8e2eba5a4054a3522fc Mon Sep 17 00:00:00 2001 From: Kyle Pelletier Date: Sun, 23 Nov 2025 05:02:01 -0800 Subject: [PATCH 15/15] Adds early exit check if no fields are updateable by triggering user --- .../classes/SummitEventsRtfLinkPipeline.cls | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls index 1d802d59..3394e84d 100644 --- a/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls +++ b/force-app/main/default/classes/SummitEventsRtfLinkPipeline.cls @@ -50,10 +50,27 @@ public without sharing class SummitEventsRtfLinkPipeline { List fields = new List(); String query = ''; + Summit_Events__c schemaObject = new Summit_Events__c(); + Map fieldMap = + schemaObject + .getSObjectType() + .getDescribe() + .fields + .getMap(); + for(String field : RICHTEXT_FIELDS) { - fields.add(namespace + field); + + Boolean isUpdateable = fieldMap.get(namespace + field).getDescribe().isUpdateable(); + + if(isUpdateable) { + fields.add(namespace + field); + } } + if(fields.isEmpty()) { + throw new SummitEventsException('No RTF fields found or user lacks sufficient permissions to update fields'); + } + String fieldList = String.join(fields, ', '); query = 'SELECT Name, ' + fieldList + ' FROM ' + namespace + 'Summit_Events__c WHERE Id IN :idList'; freshEventList = Database.query(query);