@salesforce/afv-skills 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +6 -5
  2. package/skills/creating-webapp/SKILL.md +0 -2
  3. package/skills/generating-apex/SKILL.md +253 -0
  4. package/skills/generating-apex/assets/abstract.cls +128 -0
  5. package/skills/generating-apex/assets/batch.cls +125 -0
  6. package/skills/generating-apex/assets/domain.cls +102 -0
  7. package/skills/generating-apex/assets/dto.cls +108 -0
  8. package/skills/generating-apex/assets/exception.cls +51 -0
  9. package/skills/generating-apex/assets/interface.cls +25 -0
  10. package/skills/generating-apex/assets/queueable.cls +92 -0
  11. package/skills/generating-apex/assets/schedulable.cls +75 -0
  12. package/skills/generating-apex/assets/selector.cls +92 -0
  13. package/skills/generating-apex/assets/service.cls +69 -0
  14. package/skills/generating-apex/assets/utility.cls +97 -0
  15. package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
  16. package/skills/generating-apex/references/AccountSelector.cls +193 -0
  17. package/skills/generating-apex/references/AccountService.cls +201 -0
  18. package/skills/generating-apex-test/SKILL.md +108 -0
  19. package/skills/generating-apex-test/assets/test-class-template.cls +124 -0
  20. package/skills/generating-apex-test/assets/test-data-factory-template.cls +112 -0
  21. package/skills/generating-apex-test/references/assertion-patterns.md +165 -0
  22. package/skills/generating-apex-test/references/async-testing.md +276 -0
  23. package/skills/generating-apex-test/references/mocking-patterns.md +219 -0
  24. package/skills/generating-apex-test/references/test-data-factory.md +176 -0
  25. package/skills/generating-experience-lwr-site/SKILL.md +42 -16
  26. package/skills/generating-experience-lwr-site/docs/configure-content-brandingSet.md +17 -7
  27. package/skills/generating-experience-lwr-site/docs/configure-content-themeLayout.md +2 -1
  28. package/skills/generating-experience-lwr-site/docs/configure-content-view.md +3 -3
  29. package/skills/generating-experience-react-site/SKILL.md +11 -0
  30. package/skills/generating-flexipage/SKILL.md +39 -57
  31. package/skills/searching-media/SKILL.md +342 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @description Data Transfer Object for {describe the data this DTO represents}.
3
+ * Used to pass structured data between layers without exposing SObjects.
4
+ * Serialization-friendly for use with JSON.serialize/deserialize and API responses.
5
+ * @author Generated by Apex Class Writer Skill
6
+ *
7
+ * @example
8
+ * // Create from constructor
9
+ * {ClassName} dto = new {ClassName}('value1', 42);
10
+ *
11
+ * // Deserialize from JSON
12
+ * {ClassName} dto = ({ClassName}) JSON.deserialize(jsonString, {ClassName}.class);
13
+ */
14
+ public with sharing class {ClassName} {
15
+
16
+ // ─── Properties ──────────────────────────────────────────────────────
17
+
18
+ /** @description {Describe this property} */
19
+ public String name { get; set; }
20
+
21
+ /** @description {Describe this property} */
22
+ public Id recordId { get; set; }
23
+
24
+ /** @description {Describe this property} */
25
+ public Boolean isActive { get; set; }
26
+
27
+ /** @description {Describe this property} */
28
+ public List<String> tags { get; set; }
29
+
30
+ // TODO: Add additional properties as needed
31
+
32
+ // ─── Constructors ────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * @description No-arg constructor for deserialization compatibility
36
+ */
37
+ public {ClassName}() {
38
+ this.tags = new List<String>();
39
+ this.isActive = false;
40
+ }
41
+
42
+ /**
43
+ * @description Parameterized constructor for convenience
44
+ * @param name The name value
45
+ * @param recordId The associated record Id
46
+ */
47
+ public {ClassName}(String name, Id recordId) {
48
+ this();
49
+ this.name = name;
50
+ this.recordId = recordId;
51
+ }
52
+
53
+ // ─── Factory Methods ─────────────────────────────────────────────────
54
+
55
+ /**
56
+ * @description Creates a DTO instance from an SObject record
57
+ * @param record The source {SObject} record
58
+ * @return A populated {ClassName} instance
59
+ */
60
+ public static {ClassName} fromSObject(SObject record) {
61
+ if (record == null) {
62
+ return new {ClassName}();
63
+ }
64
+
65
+ {ClassName} dto = new {ClassName}();
66
+ dto.recordId = record.Id;
67
+ dto.name = (String) record.get('Name');
68
+ // TODO: Map additional fields
69
+
70
+ return dto;
71
+ }
72
+
73
+ /**
74
+ * @description Creates a list of DTOs from a list of SObject records
75
+ * @param records The source records
76
+ * @return List of populated {ClassName} instances
77
+ */
78
+ public static List<{ClassName}> fromSObjects(List<SObject> records) {
79
+ List<{ClassName}> dtos = new List<{ClassName}>();
80
+ if (records == null) {
81
+ return dtos;
82
+ }
83
+
84
+ for (SObject record : records) {
85
+ dtos.add(fromSObject(record));
86
+ }
87
+ return dtos;
88
+ }
89
+
90
+ // ─── Utility Methods ─────────────────────────────────────────────────
91
+
92
+ /**
93
+ * @description Serializes this DTO to a JSON string
94
+ * @return JSON representation of this DTO
95
+ */
96
+ public String toJson() {
97
+ return JSON.serialize(this);
98
+ }
99
+
100
+ /**
101
+ * @description Deserializes a JSON string into a {ClassName} instance
102
+ * @param jsonString The JSON string to deserialize
103
+ * @return A {ClassName} instance
104
+ */
105
+ public static {ClassName} fromJson(String jsonString) {
106
+ return ({ClassName}) JSON.deserialize(jsonString, {ClassName}.class);
107
+ }
108
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @description Custom exception for {describe when this exception is thrown}.
3
+ * Use this exception to signal domain-specific errors that callers
4
+ * can catch and handle distinctly from system exceptions.
5
+ * @author Generated by Apex Class Writer Skill
6
+ *
7
+ * @example
8
+ * throw new {ClassName}('Account merge failed: duplicate detected.');
9
+ *
10
+ * // Wrap a caught exception
11
+ * try {
12
+ * // ... risky operation
13
+ * } catch (DmlException e) {
14
+ * throw new {ClassName}('DML failed during account merge: ' + e.getMessage());
15
+ * }
16
+ */
17
+ public with sharing class {ClassName} extends Exception {
18
+ // Apex custom exceptions automatically inherit:
19
+ // - getMessage()
20
+ // - getCause()
21
+ // - getStackTraceString()
22
+ // - setMessage(String)
23
+ // - initCause(Exception)
24
+ //
25
+ // And support these constructor patterns:
26
+ // - new {ClassName}()
27
+ // - new {ClassName}('message')
28
+ // - new {ClassName}(causeException)
29
+ // - new {ClassName}('message', causeException)
30
+ //
31
+ // Note: Apex does NOT support custom constructors on Exception subclasses.
32
+ // To add context, use the message string or create a wrapper pattern:
33
+ //
34
+ // Example wrapper pattern (if you need structured error data):
35
+ //
36
+ // public class {ClassName}Detail {
37
+ // public String errorCode;
38
+ // public List<Id> failedRecordIds;
39
+ // public String detail;
40
+ //
41
+ // public {ClassName}Detail(String errorCode, List<Id> failedRecordIds, String detail) {
42
+ // this.errorCode = errorCode;
43
+ // this.failedRecordIds = failedRecordIds;
44
+ // this.detail = detail;
45
+ // }
46
+ //
47
+ // public override String toString() {
48
+ // return '[' + errorCode + '] ' + detail + ' (Records: ' + failedRecordIds + ')';
49
+ // }
50
+ // }
51
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @description Interface for {describe the capability or contract this interface defines}.
3
+ * Implement this interface to provide {describe what implementations do}.
4
+ * @author Generated by Apex Class Writer Skill
5
+ *
6
+ * @example
7
+ * public class EmailNotificationService implements {InterfaceName} {
8
+ * public void execute(Map<String, Object> params) {
9
+ * // Implementation
10
+ * }
11
+ * }
12
+ */
13
+ public interface {InterfaceName} {
14
+
15
+ /**
16
+ * @description {Describe what this method should do}
17
+ * @param params {Describe the parameter}
18
+ * @return {Describe the return value}
19
+ */
20
+ // TODO: Define interface methods
21
+ // Example:
22
+ // void execute(Map<String, Object> params);
23
+ // Boolean isEligible(SObject record);
24
+ // List<SObject> process(List<SObject> records);
25
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @description Queueable Apex class for {describe the async operation}.
3
+ * Accepts data through the constructor for stateful processing.
4
+ * Optionally implements Database.AllowsCallouts for external integrations.
5
+ * @author Generated by Apex Class Writer Skill
6
+ *
7
+ * @example
8
+ * // Enqueue the job
9
+ * Id jobId = System.enqueueJob(new {ClassName}(recordIds));
10
+ */
11
+ public with sharing class {ClassName} implements Queueable /*, Database.AllowsCallouts */ {
12
+
13
+ // ─── Constants ───────────────────────────────────────────────────────
14
+ private static final Integer MAX_CHAIN_DEPTH = 5;
15
+
16
+ // ─── Instance Variables (Stateful) ───────────────────────────────────
17
+ private Set<Id> recordIds;
18
+ private Integer chainDepth;
19
+
20
+ // ─── Constructors ────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * @description Creates a new queueable job to process the specified records
24
+ * @param recordIds Set of record Ids to process
25
+ */
26
+ public {ClassName}(Set<Id> recordIds) {
27
+ this(recordIds, 0);
28
+ }
29
+
30
+ /**
31
+ * @description Creates a new queueable job with chain depth tracking
32
+ * @param recordIds Set of record Ids to process
33
+ * @param chainDepth Current depth in the queueable chain
34
+ */
35
+ public {ClassName}(Set<Id> recordIds, Integer chainDepth) {
36
+ this.recordIds = recordIds ?? new Set<Id>();
37
+ this.chainDepth = chainDepth;
38
+ }
39
+
40
+ // ─── Queueable Interface ─────────────────────────────────────────────
41
+
42
+ /**
43
+ * @description Executes the asynchronous work
44
+ * @param context The queueable context
45
+ */
46
+ public void execute(QueueableContext context) {
47
+ if (this.recordIds.isEmpty()) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ // TODO: Implement the async processing logic
53
+ // List<{SObject}> records = {SObject}Selector.selectByIds(this.recordIds);
54
+ // ... process records ...
55
+
56
+ // Chain to next job if there's more work and we haven't hit the depth limit
57
+ chainIfNeeded();
58
+
59
+ } catch (Exception e) {
60
+ handleError(context.getJobId(), e);
61
+ }
62
+ }
63
+
64
+ // ─── Private Helpers ─────────────────────────────────────────────────
65
+
66
+ /**
67
+ * @description Chains to the next queueable job if needed, with depth guard
68
+ */
69
+ private void chainIfNeeded() {
70
+ // TODO: Determine if chaining is needed (e.g., remaining records to process)
71
+ Set<Id> remainingIds = new Set<Id>();
72
+
73
+ if (!remainingIds.isEmpty() && this.chainDepth < MAX_CHAIN_DEPTH) {
74
+ if (!Test.isRunningTest()) {
75
+ System.enqueueJob(new {ClassName}(remainingIds, this.chainDepth + 1));
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * @description Handles errors during execution
82
+ * @param jobId The async job Id
83
+ * @param e The exception that occurred
84
+ */
85
+ private void handleError(Id jobId, Exception e) {
86
+ System.debug(LoggingLevel.ERROR,
87
+ '{ClassName} failed (Job: ' + jobId + '): ' +
88
+ e.getMessage() + '\n' + e.getStackTraceString()
89
+ );
90
+ // TODO: Persist error to a log object or send notification
91
+ }
92
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @description Schedulable Apex class for {describe the scheduled operation}.
3
+ * Delegates heavy processing to a Batch or Queueable job.
4
+ * Keep execute() lightweight — it should only launch other jobs.
5
+ * @author Generated by Apex Class Writer Skill
6
+ *
7
+ * @example
8
+ * // Schedule to run daily at 2 AM
9
+ * String jobId = System.schedule(
10
+ * '{ClassName} - Daily',
11
+ * {ClassName}.CRON_DAILY_2AM,
12
+ * new {ClassName}()
13
+ * );
14
+ *
15
+ * // Or use the convenience method
16
+ * String jobId = {ClassName}.scheduleDaily();
17
+ */
18
+ public with sharing class {ClassName} implements Schedulable {
19
+
20
+ // ─── CRON Expressions ────────────────────────────────────────────────
21
+ // Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
22
+
23
+ /** @description Runs daily at 2:00 AM */
24
+ public static final String CRON_DAILY_2AM = '0 0 2 * * ?';
25
+
26
+ /** @description Runs every weekday at 6:00 AM */
27
+ public static final String CRON_WEEKDAYS_6AM = '0 0 6 ? * MON-FRI';
28
+
29
+ /** @description Runs hourly at the top of the hour */
30
+ public static final String CRON_HOURLY = '0 0 * * * ?';
31
+
32
+ // ─── Schedulable Interface ───────────────────────────────────────────
33
+
34
+ /**
35
+ * @description Entry point for the scheduled execution.
36
+ * Delegates to a Batch or Queueable for the actual work.
37
+ * @param sc The schedulable context
38
+ */
39
+ public void execute(SchedulableContext sc) {
40
+ // Option A: Launch a Batch job
41
+ // Database.executeBatch(new {BatchClassName}(), 200);
42
+
43
+ // Option B: Launch a Queueable job
44
+ // System.enqueueJob(new {QueueableClassName}(params));
45
+
46
+ // TODO: Implement delegation to appropriate async job
47
+ }
48
+
49
+ // ─── Convenience Scheduling Methods ──────────────────────────────────
50
+
51
+ /**
52
+ * @description Schedules this job to run daily at 2 AM
53
+ * @return The scheduled job Id
54
+ */
55
+ public static String scheduleDaily() {
56
+ return System.schedule(
57
+ '{ClassName} - Daily 2AM',
58
+ CRON_DAILY_2AM,
59
+ new {ClassName}()
60
+ );
61
+ }
62
+
63
+ /**
64
+ * @description Aborts this scheduled job by name
65
+ * @param jobName The name used when scheduling
66
+ */
67
+ public static void abort(String jobName) {
68
+ for (CronTrigger ct : [
69
+ SELECT Id FROM CronTrigger
70
+ WHERE CronJobDetail.Name = :jobName
71
+ ]) {
72
+ System.abortJob(ct.Id);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @description Selector class for {SObject} queries.
3
+ * Encapsulates all SOQL for {SObject} records.
4
+ * All methods return bulkified results (Lists or Maps).
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public with sharing class {SObject}Selector {
8
+
9
+ // ─── Field Lists ─────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * @description Returns the default set of fields to query for {SObject}.
13
+ * Centralizes field references to keep queries DRY.
14
+ * @return Comma-separated field list as a String
15
+ */
16
+ private static String getDefaultFields() {
17
+ return String.join(
18
+ new List<String>{
19
+ 'Id',
20
+ 'Name',
21
+ 'CreatedDate',
22
+ 'LastModifiedDate'
23
+ // TODO: Add additional fields here
24
+ },
25
+ ', '
26
+ );
27
+ }
28
+
29
+ // ─── Query Methods ───────────────────────────────────────────────────
30
+
31
+ /**
32
+ * @description Selects {SObject} records by their Ids
33
+ * @param recordIds Set of {SObject} Ids to query
34
+ * @return List of {SObject} records matching the provided Ids
35
+ * @example
36
+ * Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
37
+ * List<{SObject}> results = {SObject}Selector.selectByIds(ids);
38
+ */
39
+ public static List<{SObject}> selectByIds(Set<Id> recordIds) {
40
+ if (recordIds == null || recordIds.isEmpty()) {
41
+ return new List<{SObject}>();
42
+ }
43
+
44
+ return Database.query(
45
+ 'SELECT ' + getDefaultFields() +
46
+ ' FROM {SObject}' +
47
+ ' WHERE Id IN :recordIds'
48
+ );
49
+ }
50
+
51
+ /**
52
+ * @description Selects {SObject} records as a Map keyed by Id
53
+ * @param recordIds Set of {SObject} Ids to query
54
+ * @return Map of Id to {SObject}
55
+ */
56
+ public static Map<Id, {SObject}> selectMapByIds(Set<Id> recordIds) {
57
+ return new Map<Id, {SObject}>(selectByIds(recordIds));
58
+ }
59
+
60
+ /**
61
+ * @description Selects {SObject} records by a specific field value
62
+ * @param fieldName API name of the field to filter on
63
+ * @param values Set of values to match
64
+ * @return List of matching {SObject} records
65
+ * @example
66
+ * List<{SObject}> results = {SObject}Selector.selectByField('Status__c', new Set<String>{ 'Active' });
67
+ */
68
+ public static List<{SObject}> selectByField(String fieldName, Set<String> values) {
69
+ if (String.isBlank(fieldName) || values == null || values.isEmpty()) {
70
+ return new List<{SObject}>();
71
+ }
72
+
73
+ // Validate field name to prevent SOQL injection
74
+ Schema.SObjectField field = Schema.SObjectType.{SObject}.fields.getMap().get(fieldName);
75
+ if (field == null) {
76
+ throw new QueryException('Invalid field name: ' + fieldName);
77
+ }
78
+
79
+ return Database.query(
80
+ 'SELECT ' + getDefaultFields() +
81
+ ' FROM {SObject}' +
82
+ ' WHERE ' + String.escapeSingleQuotes(fieldName) + ' IN :values'
83
+ );
84
+ }
85
+
86
+ // ─── Exception ───────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * @description Custom exception for query errors
90
+ */
91
+ public class QueryException extends Exception {}
92
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @description Service class for {SObject} business logic.
3
+ * Follows separation of concerns: delegates queries to {SObject}Selector
4
+ * and SObject manipulation to {SObject}Domain where applicable.
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public with sharing class {SObject}Service {
8
+
9
+ // ─── Constants ───────────────────────────────────────────────────────
10
+ private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
11
+
12
+ // ─── Public API ──────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * @description {Describe the primary operation}
16
+ * @param recordIds Set of {SObject} Ids to process
17
+ * @return List of processed {SObject} records
18
+ * @throws {SObject}ServiceException if processing fails
19
+ * @example
20
+ * Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
21
+ * List<{SObject}> results = {SObject}Service.process{SObject}s(ids);
22
+ */
23
+ public static List<{SObject}> process{SObject}s(Set<Id> recordIds) {
24
+ // Guard clause
25
+ if (recordIds == null || recordIds.isEmpty()) {
26
+ throw new {SObject}ServiceException(ERROR_NULL_INPUT);
27
+ }
28
+
29
+ // Query via Selector
30
+ List<{SObject}> records = {SObject}Selector.selectByIds(recordIds);
31
+
32
+ // Apply business logic
33
+ // TODO: Implement business logic here
34
+
35
+ // DML
36
+ try {
37
+ update records;
38
+ } catch (DmlException e) {
39
+ throw new {SObject}ServiceException(
40
+ 'Failed to update {SObject} records: ' + e.getMessage()
41
+ );
42
+ }
43
+
44
+ return records;
45
+ }
46
+
47
+ // ─── Convenience Overloads ───────────────────────────────────────────
48
+
49
+ /**
50
+ * @description Single-record convenience overload
51
+ * @param recordId The {SObject} Id to process
52
+ * @return The processed {SObject} record
53
+ */
54
+ public static {SObject} process{SObject}(Id recordId) {
55
+ List<{SObject}> results = process{SObject}s(new Set<Id>{ recordId });
56
+ return results.isEmpty() ? null : results[0];
57
+ }
58
+
59
+ // ─── Private Helpers ─────────────────────────────────────────────────
60
+
61
+ // TODO: Add private helper methods as needed
62
+
63
+ // ─── Exception ───────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * @description Custom exception for {SObject}Service errors
67
+ */
68
+ public class {SObject}ServiceException extends Exception {}
69
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @description Utility class for {describe the category of utilities: String, Date, Collection, etc.}.
3
+ * All methods are static and side-effect-free (no SOQL, no DML).
4
+ * Private constructor prevents instantiation.
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public with sharing class {ClassName} {
8
+
9
+ // ─── Private Constructor ─────────────────────────────────────────────
10
+
11
+ /**
12
+ * @description Prevents instantiation — use static methods only
13
+ */
14
+ @TestVisible
15
+ private {ClassName}() {
16
+ // Utility class — do not instantiate
17
+ }
18
+
19
+ // ─── Public Methods ──────────────────────────────────────────────────
20
+
21
+ // TODO: Add utility methods below. Examples:
22
+
23
+ /**
24
+ * @description Safely converts a String to an Integer, returning a default if parsing fails
25
+ * @param value The String to parse
26
+ * @param defaultValue The fallback value if parsing fails
27
+ * @return The parsed Integer or the default value
28
+ * @example
29
+ * Integer result = {ClassName}.safeParseInteger('42', 0); // returns 42
30
+ * Integer result = {ClassName}.safeParseInteger('abc', 0); // returns 0
31
+ */
32
+ public static Integer safeParseInteger(String value, Integer defaultValue) {
33
+ if (String.isBlank(value)) {
34
+ return defaultValue;
35
+ }
36
+ try {
37
+ return Integer.valueOf(value.trim());
38
+ } catch (TypeException e) {
39
+ return defaultValue;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @description Chunks a list into smaller sublists of the specified size.
45
+ * Useful for processing records in governor-limit-safe batches.
46
+ * @param items The list to chunk
47
+ * @param chunkSize The maximum size of each chunk
48
+ * @return A list of sublists, each containing up to chunkSize elements
49
+ * @example
50
+ * List<List<String>> chunks = {ClassName}.chunkList(myList, 200);
51
+ */
52
+ public static List<List<Object>> chunkList(List<Object> items, Integer chunkSize) {
53
+ List<List<Object>> chunks = new List<List<Object>>();
54
+ if (items == null || items.isEmpty() || chunkSize <= 0) {
55
+ return chunks;
56
+ }
57
+
58
+ List<Object> currentChunk = new List<Object>();
59
+ for (Object item : items) {
60
+ currentChunk.add(item);
61
+ if (currentChunk.size() == chunkSize) {
62
+ chunks.add(currentChunk);
63
+ currentChunk = new List<Object>();
64
+ }
65
+ }
66
+
67
+ if (!currentChunk.isEmpty()) {
68
+ chunks.add(currentChunk);
69
+ }
70
+
71
+ return chunks;
72
+ }
73
+
74
+ /**
75
+ * @description Extracts a Set of non-null field values from a list of SObjects
76
+ * @param records The SObject records to extract from
77
+ * @param fieldName The API name of the field to extract
78
+ * @return A Set of non-null String values
79
+ * @example
80
+ * Set<String> emails = {ClassName}.pluckStrings(contacts, 'Email');
81
+ */
82
+ public static Set<String> pluckStrings(List<SObject> records, String fieldName) {
83
+ Set<String> values = new Set<String>();
84
+ if (records == null || String.isBlank(fieldName)) {
85
+ return values;
86
+ }
87
+
88
+ for (SObject record : records) {
89
+ Object val = record.get(fieldName);
90
+ if (val != null) {
91
+ values.add(String.valueOf(val));
92
+ }
93
+ }
94
+
95
+ return values;
96
+ }
97
+ }