mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
initial apex-class commit
This commit is contained in:
parent
573249152f
commit
2adbaed719
253
skills/apex-development/apex-class/SKILL.md
Normal file
253
skills/apex-development/apex-class/SKILL.md
Normal file
@ -0,0 +1,253 @@
|
||||
---
|
||||
name: apex-class
|
||||
description: Generate production-ready Apex classes for Salesforce following enterprise best practices. Covers service classes, selectors, domain classes, batch/queueable/schedulable, DTOs, utilities, interfaces, abstract classes, and custom exceptions. Not for triggers or unit tests.
|
||||
---
|
||||
|
||||
# Apex Class
|
||||
|
||||
Generate well-structured, production-ready Apex classes for Salesforce development.
|
||||
|
||||
## Scope
|
||||
|
||||
**Generates:**
|
||||
- Service classes (business logic layer)
|
||||
- Selector / query classes (SOQL encapsulation)
|
||||
- Domain classes (SObject-specific logic)
|
||||
- Batch Apex classes (`Database.Batchable`)
|
||||
- Queueable Apex classes (`Queueable`, optionally `Finalizer`)
|
||||
- Schedulable Apex classes (`Schedulable`)
|
||||
- DTO / wrapper / request-response classes
|
||||
- Utility / helper classes
|
||||
- Custom exception classes
|
||||
- Interfaces and abstract classes
|
||||
|
||||
**Does NOT generate:**
|
||||
- Triggers (use a trigger framework skill)
|
||||
- Unit tests (use a test writer skill)
|
||||
- Aura controllers
|
||||
- LWC JavaScript controllers
|
||||
|
||||
---
|
||||
|
||||
## Gathering Requirements
|
||||
|
||||
Before generating code, gather these inputs from the user (ask if not provided):
|
||||
|
||||
1. **Class type** — Service, Selector, Batch, Queueable, Schedulable, Domain, DTO, Utility, Interface, Abstract, Exception
|
||||
2. **Class name** — or enough context to derive a meaningful name
|
||||
3. **SObject(s) involved** — if applicable
|
||||
4. **Business requirements** — plain-language description of what the class should do
|
||||
5. **Optional preferences:**
|
||||
- Sharing model (`with sharing`, `without sharing`, `inherited sharing`)
|
||||
- Access modifier (`public` or `global`)
|
||||
- API version (default: `62.0`)
|
||||
- Whether to include ApexDoc comments (default: yes)
|
||||
|
||||
If the user provides a clear, complete request, generate immediately without unnecessary back-and-forth.
|
||||
|
||||
---
|
||||
|
||||
## Code Standards — ALWAYS Follow These
|
||||
|
||||
### Separation of Concerns
|
||||
- **Service classes** contain business logic. They call Selectors for data and may call Domain classes for SObject-specific behavior.
|
||||
- **Selector classes** encapsulate all SOQL queries. Services never contain inline SOQL.
|
||||
- **Domain classes** encapsulate SObject-specific logic (field defaults, validation, transformation).
|
||||
- Keep classes focused on a single responsibility.
|
||||
|
||||
### Bulkification
|
||||
- All methods must operate on collections (`List`, `Set`, `Map`) by default.
|
||||
- Never accept a single SObject when a `List<SObject>` is appropriate.
|
||||
- Provide convenience overloads for single-record callers only when it makes the API cleaner, and have them delegate to the bulk method.
|
||||
|
||||
### Governor Limit Safety
|
||||
- **No SOQL or DML inside loops.** Ever.
|
||||
- Collect IDs/records first, query/DML once outside the loop.
|
||||
- Use `Limits` class checks in batch/bulk operations where appropriate.
|
||||
- Prefer `Database.insert(records, false)` with error handling in batch contexts.
|
||||
|
||||
### Sharing Model
|
||||
- Default to `with sharing` unless the user specifies otherwise.
|
||||
- If `without sharing` or `inherited sharing` is used, add an ApexDoc `@description` comment explaining WHY.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Class Type | Pattern | Example |
|
||||
|-------------|-------------------------------|-------------------------------|
|
||||
| Service | `{SObject}Service` | `AccountService` |
|
||||
| Selector | `{SObject}Selector` | `AccountSelector` |
|
||||
| Domain | `{SObject}Domain` | `OpportunityDomain` |
|
||||
| Batch | `{Descriptive}Batch` | `AccountDeduplicationBatch` |
|
||||
| Queueable | `{Descriptive}Queueable` | `ExternalSyncQueueable` |
|
||||
| Schedulable | `{Descriptive}Schedulable` | `DailyCleanupSchedulable` |
|
||||
| DTO | `{Descriptive}DTO` | `AccountMergeRequestDTO` |
|
||||
| Wrapper | `{Descriptive}Wrapper` | `OpportunityLineWrapper` |
|
||||
| Utility | `{Descriptive}Util` | `StringUtil`, `DateUtil` |
|
||||
| Interface | `I{Descriptive}` | `INotificationService` |
|
||||
| Abstract | `Abstract{Descriptive}` | `AbstractIntegrationService` |
|
||||
| Exception | `{Descriptive}Exception` | `AccountServiceException` |
|
||||
|
||||
### ApexDoc Comments
|
||||
Include ApexDoc on every `public` and `global` method and on the class itself:
|
||||
|
||||
```apex
|
||||
/**
|
||||
* @description Brief description of what the class does
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
```
|
||||
|
||||
```apex
|
||||
/**
|
||||
* @description Brief description of what the method does
|
||||
* @param paramName Description of the parameter
|
||||
* @return Description of the return value
|
||||
* @example
|
||||
* List<Account> results = AccountService.deduplicateAccounts(accountIds);
|
||||
*/
|
||||
```
|
||||
|
||||
### Null Safety
|
||||
- Use guard clauses at the top of methods for null/empty inputs.
|
||||
- Use safe navigation (`?.`) where appropriate.
|
||||
- Return empty collections rather than `null`.
|
||||
|
||||
### Constants
|
||||
- No magic strings or numbers in logic.
|
||||
- Use `private static final` constants or a dedicated constants class.
|
||||
- Use `Label.` custom labels for user-facing strings when appropriate.
|
||||
|
||||
### Custom Exceptions
|
||||
- Each service class should define or reference a corresponding custom exception.
|
||||
- Inner exception classes are preferred for simple cases: `public class AccountServiceException extends Exception {}`
|
||||
- Include meaningful error messages with context.
|
||||
|
||||
### Error Handling
|
||||
- Catch specific exceptions, not generic `Exception` unless re-throwing.
|
||||
- Log errors meaningfully (assume a logging utility exists or stub one).
|
||||
- In batch contexts, use `Database.SaveResult` / `Database.UpsertResult` with partial success.
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
For every class, produce TWO files:
|
||||
|
||||
1. **`{ClassName}.cls`** — The Apex class source code
|
||||
2. **`{ClassName}.cls-meta.xml`** — The metadata file
|
||||
|
||||
### Meta XML Template
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>{API_VERSION}</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
```
|
||||
|
||||
Default `apiVersion` is `62.0` unless the user specifies otherwise.
|
||||
|
||||
---
|
||||
|
||||
## Class Type–Specific Instructions
|
||||
|
||||
### Service Classes
|
||||
- Read and follow: `templates/service.cls`
|
||||
- Stateless — no instance variables holding mutable state
|
||||
- All public methods should be `static` unless there's a compelling reason for instance methods
|
||||
- Delegate queries to a Selector class
|
||||
- Wrap business logic errors in a custom exception
|
||||
|
||||
### Selector Classes
|
||||
- Read and follow: `templates/selector.cls`
|
||||
- One Selector per SObject (or per logical query domain)
|
||||
- Return `List<SObject>` or `Map<Id, SObject>`
|
||||
- Accept filter criteria as method parameters, not hardcoded
|
||||
- Include a private method that returns the base field list to keep DRY
|
||||
|
||||
### Domain Classes
|
||||
- Read and follow: `templates/domain.cls`
|
||||
- Encapsulate field-level defaults, derivations, and validations
|
||||
- Operate on `List<SObject>` — designed to be called from triggers or services
|
||||
- No SOQL or DML — only in-memory SObject manipulation
|
||||
|
||||
### Batch Classes
|
||||
- Read and follow: `templates/batch.cls`
|
||||
- Implement `Database.Batchable<SObject>` and optionally `Database.Stateful`
|
||||
- Use `Database.QueryLocator` in `start()` for large datasets
|
||||
- Handle partial failures in `execute()` using `Database.SaveResult`
|
||||
- Implement meaningful `finish()` — at minimum, log completion
|
||||
|
||||
### Queueable Classes
|
||||
- Read and follow: `templates/queueable.cls`
|
||||
- Implement `Queueable` and optionally `Database.AllowsCallouts`
|
||||
- Accept data through the constructor — queueables are stateful
|
||||
- For chaining, include guard logic to prevent infinite chains
|
||||
- Optionally implement `Finalizer` for error recovery
|
||||
|
||||
### Schedulable Classes
|
||||
- Read and follow: `templates/schedulable.cls`
|
||||
- Implement `Schedulable`
|
||||
- Keep `execute()` lightweight — delegate to a Batch or Queueable
|
||||
- Include a static method that returns a CRON expression for convenience
|
||||
- Document the expected schedule in ApexDoc
|
||||
|
||||
### DTO / Wrapper Classes
|
||||
- Read and follow: `templates/dto.cls`
|
||||
- Use `public` properties — no getters/setters unless validation is needed
|
||||
- Include a no-arg constructor and optionally a parameterized constructor
|
||||
- Implement `Comparable` if sorting is needed
|
||||
- Keep them serialization-friendly (no transient state unless intentional)
|
||||
|
||||
### Utility Classes
|
||||
- Read and follow: `templates/utility.cls`
|
||||
- All methods `public static`
|
||||
- Class should be `public with sharing` with a `private` constructor to prevent instantiation
|
||||
- Group related utilities (e.g., `StringUtil`, `DateUtil`, `CollectionUtil`)
|
||||
- Every method must be side-effect-free (no DML, no SOQL)
|
||||
|
||||
### Interfaces
|
||||
- Read and follow: `templates/interface.cls`
|
||||
- Define the contract clearly with ApexDoc on every method signature
|
||||
- Use meaningful names that describe the capability: `INotificationService`, `IRetryable`
|
||||
|
||||
### Abstract Classes
|
||||
- Read and follow: `templates/abstract.cls`
|
||||
- Provide default implementations for common behavior
|
||||
- Mark extension points as `protected virtual` or `protected abstract`
|
||||
- Include a concrete example in the ApexDoc showing how to extend
|
||||
|
||||
### Custom Exceptions
|
||||
- Read and follow: `templates/exception.cls`
|
||||
- Extend `Exception`
|
||||
- Keep them simple — Apex exceptions don't support custom constructors well
|
||||
- Name them descriptively: `AccountServiceException`, `IntegrationTimeoutException`
|
||||
|
||||
---
|
||||
|
||||
## Generation Workflow
|
||||
|
||||
1. Determine the class type from the user's request
|
||||
2. Read the corresponding template from `templates/`
|
||||
3. Read relevant examples from `examples/` if the class type has one
|
||||
4. Apply the user's requirements to the template pattern
|
||||
5. Generate the `.cls` file with full ApexDoc
|
||||
6. Generate the `.cls-meta.xml` file
|
||||
7. Present both files to the user
|
||||
8. Include a brief note on design decisions if any non-obvious choices were made
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- ❌ SOQL or DML inside loops
|
||||
- ❌ Hardcoded IDs or record type names (use `Schema.SObjectType` or Custom Metadata)
|
||||
- ❌ God classes that mix query + logic + DML
|
||||
- ❌ `public` fields on service classes
|
||||
- ❌ Returning `null` from methods that should return collections
|
||||
- ❌ Generic `catch (Exception e)` without re-throwing or meaningful handling
|
||||
- ❌ Business logic in Batch `start()` methods
|
||||
- ❌ Tight coupling between classes — use interfaces for extensibility
|
||||
- ❌ Magic strings or numbers
|
||||
- ❌ Methods longer than ~40 lines — break them into private helpers
|
||||
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @description Batch Apex class for identifying and flagging duplicate Account records.
|
||||
* Compares Accounts by Name and BillingPostalCode to find potential duplicates.
|
||||
* Flags duplicates by setting the Is_Potential_Duplicate__c checkbox.
|
||||
* Implements Database.Stateful to track results across batch chunks.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* // Run with default batch size (200)
|
||||
* Id jobId = AccountDeduplicationBatch.run();
|
||||
*
|
||||
* // Run with smaller batch size for complex processing
|
||||
* Database.executeBatch(new AccountDeduplicationBatch(), 50);
|
||||
*/
|
||||
public with sharing class AccountDeduplicationBatch implements Database.Batchable<SObject>, Database.Stateful {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
private static final Integer DEFAULT_BATCH_SIZE = 200;
|
||||
|
||||
// ─── Stateful Tracking ───────────────────────────────────────────────
|
||||
private Integer totalScanned = 0;
|
||||
private Integer duplicatesFound = 0;
|
||||
private Integer totalErrors = 0;
|
||||
private List<String> errorMessages = new List<String>();
|
||||
|
||||
// ─── Batchable Interface ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Queries all active Accounts that haven't already been flagged
|
||||
* @param bc The batch context
|
||||
* @return QueryLocator scoped to unflagged active Accounts
|
||||
*/
|
||||
public Database.QueryLocator start(Database.BatchableContext bc) {
|
||||
return Database.getQueryLocator([
|
||||
SELECT Id, Name, BillingPostalCode, Is_Potential_Duplicate__c
|
||||
FROM Account
|
||||
WHERE IsDeleted = FALSE
|
||||
AND Is_Potential_Duplicate__c = FALSE
|
||||
ORDER BY Name ASC
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Processes each batch by building a duplicate key and checking for matches.
|
||||
* Uses a composite key of normalized Name + BillingPostalCode.
|
||||
* @param bc The batch context
|
||||
* @param scope List of Account records in the current batch
|
||||
*/
|
||||
public void execute(Database.BatchableContext bc, List<Account> scope) {
|
||||
totalScanned += scope.size();
|
||||
|
||||
// Build duplicate keys for this batch
|
||||
Map<String, List<Account>> dupeKeyMap = new Map<String, List<Account>>();
|
||||
for (Account acct : scope) {
|
||||
String key = buildDuplicateKey(acct);
|
||||
if (String.isNotBlank(key)) {
|
||||
if (!dupeKeyMap.containsKey(key)) {
|
||||
dupeKeyMap.put(key, new List<Account>());
|
||||
}
|
||||
dupeKeyMap.get(key).add(acct);
|
||||
}
|
||||
}
|
||||
|
||||
// Flag records that share a key with another record
|
||||
List<Account> toUpdate = new List<Account>();
|
||||
for (String key : dupeKeyMap.keySet()) {
|
||||
List<Account> group = dupeKeyMap.get(key);
|
||||
if (group.size() > 1) {
|
||||
for (Account acct : group) {
|
||||
toUpdate.add(new Account(
|
||||
Id = acct.Id,
|
||||
Is_Potential_Duplicate__c = true
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!toUpdate.isEmpty()) {
|
||||
List<Database.SaveResult> results = Database.update(toUpdate, false);
|
||||
processResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Logs a summary of the deduplication batch run
|
||||
* @param bc The batch context
|
||||
*/
|
||||
public void finish(Database.BatchableContext bc) {
|
||||
String summary = String.format(
|
||||
'AccountDeduplicationBatch completed. Scanned: {0}, Duplicates flagged: {1}, Errors: {2}',
|
||||
new List<Object>{ totalScanned, duplicatesFound, totalErrors }
|
||||
);
|
||||
|
||||
System.debug(LoggingLevel.INFO, summary);
|
||||
|
||||
if (!errorMessages.isEmpty()) {
|
||||
System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n'));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Builds a normalized composite key for duplicate detection
|
||||
* @param acct The Account record
|
||||
* @return Normalized key string, or null if insufficient data
|
||||
*/
|
||||
private String buildDuplicateKey(Account acct) {
|
||||
if (String.isBlank(acct.Name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedName = acct.Name.trim().toUpperCase().replaceAll('\\s+', ' ');
|
||||
String postalCode = (acct.BillingPostalCode ?? '').trim().toUpperCase();
|
||||
|
||||
return normalizedName + '|' + postalCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Processes DML results, tracking successes and failures
|
||||
* @param results List of Database.SaveResult from update operation
|
||||
*/
|
||||
private void processResults(List<Database.SaveResult> results) {
|
||||
for (Database.SaveResult sr : results) {
|
||||
if (sr.isSuccess()) {
|
||||
duplicatesFound++;
|
||||
} else {
|
||||
totalErrors++;
|
||||
for (Database.Error err : sr.getErrors()) {
|
||||
errorMessages.add(
|
||||
'Record ' + sr.getId() + ': ' +
|
||||
err.getStatusCode() + ' - ' + err.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Static Helpers ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Convenience method to execute with default batch size
|
||||
* @return The batch job Id
|
||||
*/
|
||||
public static Id run() {
|
||||
return Database.executeBatch(new AccountDeduplicationBatch(), DEFAULT_BATCH_SIZE);
|
||||
}
|
||||
}
|
||||
193
skills/apex-development/apex-class/examples/AccountSelector.cls
Normal file
193
skills/apex-development/apex-class/examples/AccountSelector.cls
Normal file
@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @description Selector class for Account queries.
|
||||
* Encapsulates all SOQL for Account records.
|
||||
* All methods return bulkified results (Lists or Maps).
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
public with sharing class AccountSelector {
|
||||
|
||||
// ─── Field Lists ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Returns the default set of fields to query for Account.
|
||||
* Centralizes field references to keep queries DRY.
|
||||
* @return Comma-separated field list as a String
|
||||
*/
|
||||
private static String getDefaultFields() {
|
||||
return String.join(
|
||||
new List<String>{
|
||||
'Id',
|
||||
'Name',
|
||||
'AccountNumber',
|
||||
'Type',
|
||||
'Industry',
|
||||
'AnnualRevenue',
|
||||
'NumberOfEmployees',
|
||||
'Phone',
|
||||
'Website',
|
||||
'OwnerId',
|
||||
'CreatedDate',
|
||||
'LastModifiedDate'
|
||||
},
|
||||
', '
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns fields needed for billing/territory operations
|
||||
* @return Comma-separated field list as a String
|
||||
*/
|
||||
private static String getBillingFields() {
|
||||
return String.join(
|
||||
new List<String>{
|
||||
'Id',
|
||||
'Name',
|
||||
'BillingStreet',
|
||||
'BillingCity',
|
||||
'BillingState',
|
||||
'BillingPostalCode',
|
||||
'BillingCountry',
|
||||
'Territory__c'
|
||||
},
|
||||
', '
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Query Methods ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Selects Account records by their Ids
|
||||
* @param recordIds Set of Account Ids to query
|
||||
* @return List of Account records matching the provided Ids
|
||||
* @example
|
||||
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
|
||||
* List<Account> results = AccountSelector.selectByIds(ids);
|
||||
*/
|
||||
public static List<Account> selectByIds(Set<Id> recordIds) {
|
||||
if (recordIds == null || recordIds.isEmpty()) {
|
||||
return new List<Account>();
|
||||
}
|
||||
|
||||
return Database.query(
|
||||
'SELECT ' + getDefaultFields() +
|
||||
' FROM Account' +
|
||||
' WHERE Id IN :recordIds'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects Account records as a Map keyed by Id
|
||||
* @param recordIds Set of Account Ids to query
|
||||
* @return Map of Id to Account
|
||||
*/
|
||||
public static Map<Id, Account> selectMapByIds(Set<Id> recordIds) {
|
||||
return new Map<Id, Account>(selectByIds(recordIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects Accounts with billing address fields for territory assignment
|
||||
* @param recordIds Set of Account Ids to query
|
||||
* @return List of Account records with billing address fields populated
|
||||
*/
|
||||
public static List<Account> selectWithBillingAddress(Set<Id> recordIds) {
|
||||
if (recordIds == null || recordIds.isEmpty()) {
|
||||
return new List<Account>();
|
||||
}
|
||||
|
||||
return Database.query(
|
||||
'SELECT ' + getBillingFields() +
|
||||
' FROM Account' +
|
||||
' WHERE Id IN :recordIds'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects Accounts by Account Type
|
||||
* @param accountTypes Set of Account Type values to filter by
|
||||
* @return List of matching Account records
|
||||
* @example
|
||||
* List<Account> prospects = AccountSelector.selectByType(
|
||||
* new Set<String>{ 'Prospect', 'Customer - Direct' }
|
||||
* );
|
||||
*/
|
||||
public static List<Account> selectByType(Set<String> accountTypes) {
|
||||
if (accountTypes == null || accountTypes.isEmpty()) {
|
||||
return new List<Account>();
|
||||
}
|
||||
|
||||
return Database.query(
|
||||
'SELECT ' + getDefaultFields() +
|
||||
' FROM Account' +
|
||||
' WHERE Type IN :accountTypes' +
|
||||
' ORDER BY Name ASC'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects Accounts by Industry with a minimum annual revenue
|
||||
* @param industries Set of Industry values to filter by
|
||||
* @param minRevenue Minimum AnnualRevenue threshold
|
||||
* @return List of matching Account records ordered by revenue descending
|
||||
* @example
|
||||
* List<Account> techAccounts = AccountSelector.selectByIndustryAndRevenue(
|
||||
* new Set<String>{ 'Technology' },
|
||||
* 1000000
|
||||
* );
|
||||
*/
|
||||
public static List<Account> selectByIndustryAndRevenue(Set<String> industries, Decimal minRevenue) {
|
||||
if (industries == null || industries.isEmpty()) {
|
||||
return new List<Account>();
|
||||
}
|
||||
|
||||
Decimal revenueThreshold = minRevenue ?? 0;
|
||||
|
||||
return Database.query(
|
||||
'SELECT ' + getDefaultFields() +
|
||||
' FROM Account' +
|
||||
' WHERE Industry IN :industries' +
|
||||
' AND AnnualRevenue >= :revenueThreshold' +
|
||||
' ORDER BY AnnualRevenue DESC'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects Accounts with their related Contacts (subquery)
|
||||
* @param recordIds Set of Account Ids to query
|
||||
* @return List of Account records with nested Contacts
|
||||
*/
|
||||
public static List<Account> selectWithContacts(Set<Id> recordIds) {
|
||||
if (recordIds == null || recordIds.isEmpty()) {
|
||||
return new List<Account>();
|
||||
}
|
||||
|
||||
return [
|
||||
SELECT Id, Name, Type, Industry,
|
||||
(SELECT Id, FirstName, LastName, Email, Title
|
||||
FROM Contacts
|
||||
ORDER BY LastName ASC)
|
||||
FROM Account
|
||||
WHERE Id IN :recordIds
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Aggregate Queries ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Returns a count of Accounts grouped by Industry
|
||||
* @return List of AggregateResult with Industry and record count
|
||||
* @example
|
||||
* List<AggregateResult> results = AccountSelector.countByIndustry();
|
||||
* for (AggregateResult ar : results) {
|
||||
* System.debug(ar.get('Industry') + ': ' + ar.get('cnt'));
|
||||
* }
|
||||
*/
|
||||
public static List<AggregateResult> countByIndustry() {
|
||||
return [
|
||||
SELECT Industry, COUNT(Id) cnt
|
||||
FROM Account
|
||||
WHERE Industry != NULL
|
||||
GROUP BY Industry
|
||||
ORDER BY COUNT(Id) DESC
|
||||
];
|
||||
}
|
||||
}
|
||||
201
skills/apex-development/apex-class/examples/AccountService.cls
Normal file
201
skills/apex-development/apex-class/examples/AccountService.cls
Normal file
@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @description Service class for Account business logic.
|
||||
* Provides account deduplication, enrichment, and territory assignment.
|
||||
* Delegates queries to AccountSelector and SObject manipulation to AccountDomain.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
public with sharing class AccountService {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
|
||||
private static final String ERROR_MERGE_FAILED = 'Account merge failed for master Id: ';
|
||||
private static final Integer MAX_MERGE_BATCH = 3;
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Merges duplicate Account records into a master record.
|
||||
* The master record retains its field values; child records are reparented.
|
||||
* @param masterIds Map of master Account Id to Set of duplicate Account Ids to merge
|
||||
* @return List of master Account Ids that were successfully merged
|
||||
* @throws AccountServiceException if merge processing fails
|
||||
* @example
|
||||
* Map<Id, Set<Id>> mergeMap = new Map<Id, Set<Id>>{
|
||||
* masterAcctId => new Set<Id>{ dupeId1, dupeId2 }
|
||||
* };
|
||||
* List<Id> mergedIds = AccountService.mergeAccounts(mergeMap);
|
||||
*/
|
||||
public static List<Id> mergeAccounts(Map<Id, Set<Id>> mergeMap) {
|
||||
if (mergeMap == null || mergeMap.isEmpty()) {
|
||||
throw new AccountServiceException(ERROR_NULL_INPUT);
|
||||
}
|
||||
|
||||
// Collect all Ids for a single query
|
||||
Set<Id> allIds = new Set<Id>();
|
||||
allIds.addAll(mergeMap.keySet());
|
||||
for (Set<Id> dupeIds : mergeMap.values()) {
|
||||
allIds.addAll(dupeIds);
|
||||
}
|
||||
|
||||
// Query all records at once via Selector
|
||||
Map<Id, Account> accountMap = AccountSelector.selectMapByIds(allIds);
|
||||
|
||||
List<Id> successfulMerges = new List<Id>();
|
||||
List<String> errors = new List<String>();
|
||||
|
||||
for (Id masterId : mergeMap.keySet()) {
|
||||
Account master = accountMap.get(masterId);
|
||||
if (master == null) {
|
||||
errors.add('Master account not found: ' + masterId);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Account> duplicates = new List<Account>();
|
||||
for (Id dupeId : mergeMap.get(masterId)) {
|
||||
Account dupe = accountMap.get(dupeId);
|
||||
if (dupe != null) {
|
||||
duplicates.add(dupe);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Apex merge supports up to 3 records at a time
|
||||
for (List<Account> chunk : chunkAccounts(duplicates, MAX_MERGE_BATCH)) {
|
||||
Database.merge(master, chunk);
|
||||
}
|
||||
successfulMerges.add(masterId);
|
||||
} catch (DmlException e) {
|
||||
errors.add(ERROR_MERGE_FAILED + masterId + ' - ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
System.debug(LoggingLevel.WARN, 'Merge errors: ' + String.join(errors, '\n'));
|
||||
}
|
||||
|
||||
return successfulMerges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Assigns accounts to territories based on Billing State/Country.
|
||||
* Uses Custom Metadata Type (Territory_Mapping__mdt) for mappings.
|
||||
* @param accountIds Set of Account Ids to assign territories for
|
||||
* @return Number of accounts successfully updated
|
||||
* @throws AccountServiceException if territory assignment fails
|
||||
*/
|
||||
public static Integer assignTerritories(Set<Id> accountIds) {
|
||||
if (accountIds == null || accountIds.isEmpty()) {
|
||||
throw new AccountServiceException(ERROR_NULL_INPUT);
|
||||
}
|
||||
|
||||
List<Account> accounts = AccountSelector.selectWithBillingAddress(accountIds);
|
||||
Map<String, String> territoryMap = loadTerritoryMappings();
|
||||
|
||||
List<Account> toUpdate = new List<Account>();
|
||||
for (Account acct : accounts) {
|
||||
String key = buildTerritoryKey(acct.BillingState, acct.BillingCountry);
|
||||
String territory = territoryMap.get(key);
|
||||
|
||||
if (territory != null && territory != acct.Territory__c) {
|
||||
toUpdate.add(new Account(
|
||||
Id = acct.Id,
|
||||
Territory__c = territory
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!toUpdate.isEmpty()) {
|
||||
List<Database.SaveResult> results = Database.update(toUpdate, false);
|
||||
return countSuccesses(results);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─── Convenience Overloads ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Single-account territory assignment convenience method
|
||||
* @param accountId The Account Id to assign a territory for
|
||||
* @return 1 if updated, 0 if no change needed
|
||||
*/
|
||||
public static Integer assignTerritory(Id accountId) {
|
||||
return assignTerritories(new Set<Id>{ accountId });
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Loads territory mappings from Custom Metadata
|
||||
* @return Map of territory key (State:Country) to territory name
|
||||
*/
|
||||
private static Map<String, String> loadTerritoryMappings() {
|
||||
Map<String, String> mappings = new Map<String, String>();
|
||||
for (Territory_Mapping__mdt mapping : Territory_Mapping__mdt.getAll().values()) {
|
||||
String key = buildTerritoryKey(mapping.State__c, mapping.Country__c);
|
||||
mappings.put(key, mapping.Territory_Name__c);
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Builds a consistent territory lookup key
|
||||
* @param state The billing state
|
||||
* @param country The billing country
|
||||
* @return A normalized key string
|
||||
*/
|
||||
private static String buildTerritoryKey(String state, String country) {
|
||||
return (state ?? '').toUpperCase() + ':' + (country ?? '').toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Chunks a list of Accounts into sublists of the given size
|
||||
* @param accounts The accounts to chunk
|
||||
* @param chunkSize Maximum chunk size
|
||||
* @return List of account sublists
|
||||
*/
|
||||
private static List<List<Account>> chunkAccounts(List<Account> accounts, Integer chunkSize) {
|
||||
List<List<Account>> chunks = new List<List<Account>>();
|
||||
List<Account> current = new List<Account>();
|
||||
|
||||
for (Account acct : accounts) {
|
||||
current.add(acct);
|
||||
if (current.size() == chunkSize) {
|
||||
chunks.add(current);
|
||||
current = new List<Account>();
|
||||
}
|
||||
}
|
||||
if (!current.isEmpty()) {
|
||||
chunks.add(current);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Counts successful results from a DML operation
|
||||
* @param results List of Database.SaveResult
|
||||
* @return Count of successful operations
|
||||
*/
|
||||
private static Integer countSuccesses(List<Database.SaveResult> results) {
|
||||
Integer count = 0;
|
||||
for (Database.SaveResult sr : results) {
|
||||
if (sr.isSuccess()) {
|
||||
count++;
|
||||
} else {
|
||||
for (Database.Error err : sr.getErrors()) {
|
||||
System.debug(LoggingLevel.WARN,
|
||||
'Update failed for ' + sr.getId() + ': ' + err.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ─── Exception ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Custom exception for AccountService errors
|
||||
*/
|
||||
public class AccountServiceException extends Exception {}
|
||||
}
|
||||
128
skills/apex-development/apex-class/templates/abstract.cls
Normal file
128
skills/apex-development/apex-class/templates/abstract.cls
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @description Abstract base class for {describe the family of classes this serves}.
|
||||
* Provides common behavior and defines extension points for subclasses.
|
||||
* Subclasses must implement the abstract methods to provide specific behavior.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* // Extending this abstract class:
|
||||
* public class SalesforceIntegrationService extends {ClassName} {
|
||||
* protected override String getEndpoint() {
|
||||
* return 'callout:Salesforce_API/services/data/v62.0';
|
||||
* }
|
||||
*
|
||||
* protected override Map<String, String> getHeaders() {
|
||||
* return new Map<String, String>{
|
||||
* 'Content-Type' => 'application/json'
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public abstract with sharing class {ClassName} {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
private static final Integer DEFAULT_TIMEOUT_MS = 30000;
|
||||
|
||||
// ─── Protected State ─────────────────────────────────────────────────
|
||||
protected Integer timeoutMs;
|
||||
|
||||
// ─── Constructor ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Initializes the base class with default configuration
|
||||
*/
|
||||
protected {ClassName}() {
|
||||
this.timeoutMs = DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
// ─── Abstract Methods (must be implemented by subclasses) ────────────
|
||||
|
||||
/**
|
||||
* @description Returns the endpoint URL for this integration.
|
||||
* Subclasses must provide their specific endpoint.
|
||||
* @return The endpoint URL as a String
|
||||
*/
|
||||
protected abstract String getEndpoint();
|
||||
|
||||
/**
|
||||
* @description Returns the HTTP headers for this integration.
|
||||
* Subclasses define their own required headers.
|
||||
* @return Map of header name to header value
|
||||
*/
|
||||
protected abstract Map<String, String> getHeaders();
|
||||
|
||||
// ─── Virtual Methods (can be overridden by subclasses) ───────────────
|
||||
|
||||
/**
|
||||
* @description Hook called before the main operation executes.
|
||||
* Override to add pre-processing logic.
|
||||
* Default implementation does nothing.
|
||||
* @param context Map of contextual data
|
||||
*/
|
||||
protected virtual void beforeExecute(Map<String, Object> context) {
|
||||
// Default: no-op — override in subclass if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Hook called after the main operation completes.
|
||||
* Override to add post-processing logic.
|
||||
* Default implementation does nothing.
|
||||
* @param context Map of contextual data
|
||||
* @param result The result from the operation
|
||||
*/
|
||||
protected virtual void afterExecute(Map<String, Object> context, Object result) {
|
||||
// Default: no-op — override in subclass if needed
|
||||
}
|
||||
|
||||
// ─── Template Method (common workflow) ───────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Executes the operation using the template method pattern.
|
||||
* Calls beforeExecute → doExecute → afterExecute in sequence.
|
||||
* @param context Map of data needed for the operation
|
||||
* @return The result of the operation
|
||||
*/
|
||||
public Object execute(Map<String, Object> context) {
|
||||
beforeExecute(context);
|
||||
|
||||
Object result;
|
||||
try {
|
||||
result = doExecute(context);
|
||||
} catch (Exception e) {
|
||||
handleError(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
afterExecute(context, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Protected Helpers ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Core execution logic — override this for the main operation.
|
||||
* Default implementation throws — subclass must provide implementation.
|
||||
* @param context Map of data needed for the operation
|
||||
* @return The result of the operation
|
||||
*/
|
||||
protected virtual Object doExecute(Map<String, Object> context) {
|
||||
throw new UnsupportedOperationException(
|
||||
'Subclass must override doExecute()'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Error handler called when doExecute throws.
|
||||
* Override to customize error handling (e.g., logging, retry).
|
||||
* @param e The exception that was thrown
|
||||
*/
|
||||
protected virtual void handleError(Exception e) {
|
||||
System.debug(LoggingLevel.ERROR,
|
||||
this.toString() + ' error: ' + e.getMessage() + '\n' + e.getStackTraceString()
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Exception ───────────────────────────────────────────────────────
|
||||
|
||||
public class UnsupportedOperationException extends Exception {}
|
||||
}
|
||||
125
skills/apex-development/apex-class/templates/batch.cls
Normal file
125
skills/apex-development/apex-class/templates/batch.cls
Normal file
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @description Batch Apex class for {describe the batch operation}.
|
||||
* Processes {SObject} records in configurable batch sizes.
|
||||
* Implements Database.Stateful to track cumulative results across chunks.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* // Execute with default batch size
|
||||
* Database.executeBatch(new {ClassName}());
|
||||
*
|
||||
* // Execute with custom batch size
|
||||
* Database.executeBatch(new {ClassName}(), 100);
|
||||
*/
|
||||
public with sharing class {ClassName} implements Database.Batchable<SObject>, Database.Stateful {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
private static final Integer DEFAULT_BATCH_SIZE = 200;
|
||||
|
||||
// ─── Stateful Tracking ───────────────────────────────────────────────
|
||||
private Integer totalProcessed = 0;
|
||||
private Integer totalErrors = 0;
|
||||
private List<String> errorMessages = new List<String>();
|
||||
|
||||
// ─── Constructor ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Default constructor
|
||||
*/
|
||||
public {ClassName}() {
|
||||
// Default configuration
|
||||
}
|
||||
|
||||
// ─── Batchable Interface ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Defines the scope of records to process.
|
||||
* Uses Database.QueryLocator for efficient large-dataset processing.
|
||||
* @param bc The batch context
|
||||
* @return QueryLocator for the records to process
|
||||
*/
|
||||
public Database.QueryLocator start(Database.BatchableContext bc) {
|
||||
return Database.getQueryLocator([
|
||||
SELECT Id, Name
|
||||
// TODO: Add fields needed for processing
|
||||
FROM {SObject}
|
||||
// TODO: Add WHERE clause to scope the records
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Processes each batch of records.
|
||||
* Uses Database.update with allOrNone=false for partial success handling.
|
||||
* @param bc The batch context
|
||||
* @param scope List of {SObject} records in the current batch
|
||||
*/
|
||||
public void execute(Database.BatchableContext bc, List<{SObject}> scope) {
|
||||
List<{SObject}> recordsToUpdate = new List<{SObject}>();
|
||||
|
||||
for ({SObject} record : scope) {
|
||||
// TODO: Apply business logic to each record
|
||||
recordsToUpdate.add(record);
|
||||
}
|
||||
|
||||
if (!recordsToUpdate.isEmpty()) {
|
||||
List<Database.SaveResult> results = Database.update(recordsToUpdate, false);
|
||||
processResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Performs post-processing after all batches complete.
|
||||
* Logs a summary of the batch execution.
|
||||
* @param bc The batch context
|
||||
*/
|
||||
public void finish(Database.BatchableContext bc) {
|
||||
String summary = String.format(
|
||||
'{0} completed. Processed: {1}, Errors: {2}',
|
||||
new List<Object>{
|
||||
{ClassName}.class.getName(),
|
||||
totalProcessed,
|
||||
totalErrors
|
||||
}
|
||||
);
|
||||
|
||||
System.debug(LoggingLevel.INFO, summary);
|
||||
|
||||
if (!errorMessages.isEmpty()) {
|
||||
System.debug(LoggingLevel.ERROR, 'Error details: ' + String.join(errorMessages, '\n'));
|
||||
}
|
||||
|
||||
// TODO: Send completion notification email or post to chatter if needed
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Processes Database.SaveResult list, tracking successes and failures
|
||||
* @param results List of SaveResult from a DML operation
|
||||
*/
|
||||
private void processResults(List<Database.SaveResult> results) {
|
||||
for (Database.SaveResult result : results) {
|
||||
if (result.isSuccess()) {
|
||||
totalProcessed++;
|
||||
} else {
|
||||
totalErrors++;
|
||||
for (Database.Error err : result.getErrors()) {
|
||||
errorMessages.add(
|
||||
'Record ' + result.getId() + ': ' +
|
||||
err.getStatusCode() + ' - ' + err.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Static Helpers ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Convenience method to execute with default batch size
|
||||
* @return The batch job Id
|
||||
*/
|
||||
public static Id run() {
|
||||
return Database.executeBatch(new {ClassName}(), DEFAULT_BATCH_SIZE);
|
||||
}
|
||||
}
|
||||
102
skills/apex-development/apex-class/templates/domain.cls
Normal file
102
skills/apex-development/apex-class/templates/domain.cls
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @description Domain class for {SObject}.
|
||||
* Encapsulates field-level defaults, derivations, and validations.
|
||||
* Operates only on in-memory SObject data — no SOQL or DML.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
public with sharing class {SObject}Domain {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
// TODO: Add constants for default values, statuses, etc.
|
||||
|
||||
// ─── Field Defaults ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Applies default field values to new {SObject} records.
|
||||
* Call this before insert to ensure consistent defaults.
|
||||
* @param records List of {SObject} records to apply defaults to
|
||||
*/
|
||||
public static void applyDefaults(List<{SObject}> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ({SObject} record : records) {
|
||||
// TODO: Set default field values
|
||||
// Example:
|
||||
// if (record.Status__c == null) {
|
||||
// record.Status__c = DEFAULT_STATUS;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Derivations ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Derives calculated field values based on other fields.
|
||||
* Call this before insert and before update.
|
||||
* @param records List of {SObject} records to derive values for
|
||||
*/
|
||||
public static void deriveFields(List<{SObject}> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ({SObject} record : records) {
|
||||
// TODO: Derive calculated field values
|
||||
// Example:
|
||||
// record.FullAddress__c = buildFullAddress(record);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Validations ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Validates {SObject} records and adds errors for any violations.
|
||||
* Call this before insert and before update.
|
||||
* @param records List of {SObject} records to validate
|
||||
*/
|
||||
public static void validate(List<{SObject}> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ({SObject} record : records) {
|
||||
// TODO: Add validation rules
|
||||
// Example:
|
||||
// if (String.isBlank(record.Name)) {
|
||||
// record.addError('Name is required.');
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Comparisons ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Determines which fields have changed between old and new record versions.
|
||||
* Useful in before update context.
|
||||
* @param oldRecord The previous version of the record
|
||||
* @param newRecord The current version of the record
|
||||
* @return Set of field API names that have changed
|
||||
*/
|
||||
public static Set<String> getChangedFields({SObject} oldRecord, {SObject} newRecord) {
|
||||
Set<String> changedFields = new Set<String>();
|
||||
|
||||
if (oldRecord == null || newRecord == null) {
|
||||
return changedFields;
|
||||
}
|
||||
|
||||
Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.{SObject}.fields.getMap();
|
||||
for (String fieldName : fieldMap.keySet()) {
|
||||
if (oldRecord.get(fieldName) != newRecord.get(fieldName)) {
|
||||
changedFields.add(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return changedFields;
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────
|
||||
|
||||
// TODO: Add private helper methods as needed
|
||||
}
|
||||
108
skills/apex-development/apex-class/templates/dto.cls
Normal file
108
skills/apex-development/apex-class/templates/dto.cls
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @description Data Transfer Object for {describe the data this DTO represents}.
|
||||
* Used to pass structured data between layers without exposing SObjects.
|
||||
* Serialization-friendly for use with JSON.serialize/deserialize and API responses.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* // Create from constructor
|
||||
* {ClassName} dto = new {ClassName}('value1', 42);
|
||||
*
|
||||
* // Deserialize from JSON
|
||||
* {ClassName} dto = ({ClassName}) JSON.deserialize(jsonString, {ClassName}.class);
|
||||
*/
|
||||
public with sharing class {ClassName} {
|
||||
|
||||
// ─── Properties ──────────────────────────────────────────────────────
|
||||
|
||||
/** @description {Describe this property} */
|
||||
public String name { get; set; }
|
||||
|
||||
/** @description {Describe this property} */
|
||||
public Id recordId { get; set; }
|
||||
|
||||
/** @description {Describe this property} */
|
||||
public Boolean isActive { get; set; }
|
||||
|
||||
/** @description {Describe this property} */
|
||||
public List<String> tags { get; set; }
|
||||
|
||||
// TODO: Add additional properties as needed
|
||||
|
||||
// ─── Constructors ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description No-arg constructor for deserialization compatibility
|
||||
*/
|
||||
public {ClassName}() {
|
||||
this.tags = new List<String>();
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Parameterized constructor for convenience
|
||||
* @param name The name value
|
||||
* @param recordId The associated record Id
|
||||
*/
|
||||
public {ClassName}(String name, Id recordId) {
|
||||
this();
|
||||
this.name = name;
|
||||
this.recordId = recordId;
|
||||
}
|
||||
|
||||
// ─── Factory Methods ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Creates a DTO instance from an SObject record
|
||||
* @param record The source {SObject} record
|
||||
* @return A populated {ClassName} instance
|
||||
*/
|
||||
public static {ClassName} fromSObject(SObject record) {
|
||||
if (record == null) {
|
||||
return new {ClassName}();
|
||||
}
|
||||
|
||||
{ClassName} dto = new {ClassName}();
|
||||
dto.recordId = record.Id;
|
||||
dto.name = (String) record.get('Name');
|
||||
// TODO: Map additional fields
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a list of DTOs from a list of SObject records
|
||||
* @param records The source records
|
||||
* @return List of populated {ClassName} instances
|
||||
*/
|
||||
public static List<{ClassName}> fromSObjects(List<SObject> records) {
|
||||
List<{ClassName}> dtos = new List<{ClassName}>();
|
||||
if (records == null) {
|
||||
return dtos;
|
||||
}
|
||||
|
||||
for (SObject record : records) {
|
||||
dtos.add(fromSObject(record));
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
// ─── Utility Methods ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Serializes this DTO to a JSON string
|
||||
* @return JSON representation of this DTO
|
||||
*/
|
||||
public String toJson() {
|
||||
return JSON.serialize(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Deserializes a JSON string into a {ClassName} instance
|
||||
* @param jsonString The JSON string to deserialize
|
||||
* @return A {ClassName} instance
|
||||
*/
|
||||
public static {ClassName} fromJson(String jsonString) {
|
||||
return ({ClassName}) JSON.deserialize(jsonString, {ClassName}.class);
|
||||
}
|
||||
}
|
||||
51
skills/apex-development/apex-class/templates/exception.cls
Normal file
51
skills/apex-development/apex-class/templates/exception.cls
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @description Custom exception for {describe when this exception is thrown}.
|
||||
* Use this exception to signal domain-specific errors that callers
|
||||
* can catch and handle distinctly from system exceptions.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* throw new {ClassName}('Account merge failed: duplicate detected.');
|
||||
*
|
||||
* // Wrap a caught exception
|
||||
* try {
|
||||
* // ... risky operation
|
||||
* } catch (DmlException e) {
|
||||
* throw new {ClassName}('DML failed during account merge: ' + e.getMessage());
|
||||
* }
|
||||
*/
|
||||
public with sharing class {ClassName} extends Exception {
|
||||
// Apex custom exceptions automatically inherit:
|
||||
// - getMessage()
|
||||
// - getCause()
|
||||
// - getStackTraceString()
|
||||
// - setMessage(String)
|
||||
// - initCause(Exception)
|
||||
//
|
||||
// And support these constructor patterns:
|
||||
// - new {ClassName}()
|
||||
// - new {ClassName}('message')
|
||||
// - new {ClassName}(causeException)
|
||||
// - new {ClassName}('message', causeException)
|
||||
//
|
||||
// Note: Apex does NOT support custom constructors on Exception subclasses.
|
||||
// To add context, use the message string or create a wrapper pattern:
|
||||
//
|
||||
// Example wrapper pattern (if you need structured error data):
|
||||
//
|
||||
// public class {ClassName}Detail {
|
||||
// public String errorCode;
|
||||
// public List<Id> failedRecordIds;
|
||||
// public String detail;
|
||||
//
|
||||
// public {ClassName}Detail(String errorCode, List<Id> failedRecordIds, String detail) {
|
||||
// this.errorCode = errorCode;
|
||||
// this.failedRecordIds = failedRecordIds;
|
||||
// this.detail = detail;
|
||||
// }
|
||||
//
|
||||
// public override String toString() {
|
||||
// return '[' + errorCode + '] ' + detail + ' (Records: ' + failedRecordIds + ')';
|
||||
// }
|
||||
// }
|
||||
}
|
||||
25
skills/apex-development/apex-class/templates/interface.cls
Normal file
25
skills/apex-development/apex-class/templates/interface.cls
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @description Interface for {describe the capability or contract this interface defines}.
|
||||
* Implement this interface to provide {describe what implementations do}.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* public class EmailNotificationService implements {InterfaceName} {
|
||||
* public void execute(Map<String, Object> params) {
|
||||
* // Implementation
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public interface {InterfaceName} {
|
||||
|
||||
/**
|
||||
* @description {Describe what this method should do}
|
||||
* @param params {Describe the parameter}
|
||||
* @return {Describe the return value}
|
||||
*/
|
||||
// TODO: Define interface methods
|
||||
// Example:
|
||||
// void execute(Map<String, Object> params);
|
||||
// Boolean isEligible(SObject record);
|
||||
// List<SObject> process(List<SObject> records);
|
||||
}
|
||||
92
skills/apex-development/apex-class/templates/queueable.cls
Normal file
92
skills/apex-development/apex-class/templates/queueable.cls
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @description Queueable Apex class for {describe the async operation}.
|
||||
* Accepts data through the constructor for stateful processing.
|
||||
* Optionally implements Database.AllowsCallouts for external integrations.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* // Enqueue the job
|
||||
* Id jobId = System.enqueueJob(new {ClassName}(recordIds));
|
||||
*/
|
||||
public with sharing class {ClassName} implements Queueable /*, Database.AllowsCallouts */ {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
private static final Integer MAX_CHAIN_DEPTH = 5;
|
||||
|
||||
// ─── Instance Variables (Stateful) ───────────────────────────────────
|
||||
private Set<Id> recordIds;
|
||||
private Integer chainDepth;
|
||||
|
||||
// ─── Constructors ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Creates a new queueable job to process the specified records
|
||||
* @param recordIds Set of record Ids to process
|
||||
*/
|
||||
public {ClassName}(Set<Id> recordIds) {
|
||||
this(recordIds, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a new queueable job with chain depth tracking
|
||||
* @param recordIds Set of record Ids to process
|
||||
* @param chainDepth Current depth in the queueable chain
|
||||
*/
|
||||
public {ClassName}(Set<Id> recordIds, Integer chainDepth) {
|
||||
this.recordIds = recordIds ?? new Set<Id>();
|
||||
this.chainDepth = chainDepth;
|
||||
}
|
||||
|
||||
// ─── Queueable Interface ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Executes the asynchronous work
|
||||
* @param context The queueable context
|
||||
*/
|
||||
public void execute(QueueableContext context) {
|
||||
if (this.recordIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement the async processing logic
|
||||
// List<{SObject}> records = {SObject}Selector.selectByIds(this.recordIds);
|
||||
// ... process records ...
|
||||
|
||||
// Chain to next job if there's more work and we haven't hit the depth limit
|
||||
chainIfNeeded();
|
||||
|
||||
} catch (Exception e) {
|
||||
handleError(context.getJobId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Chains to the next queueable job if needed, with depth guard
|
||||
*/
|
||||
private void chainIfNeeded() {
|
||||
// TODO: Determine if chaining is needed (e.g., remaining records to process)
|
||||
Set<Id> remainingIds = new Set<Id>();
|
||||
|
||||
if (!remainingIds.isEmpty() && this.chainDepth < MAX_CHAIN_DEPTH) {
|
||||
if (!Test.isRunningTest()) {
|
||||
System.enqueueJob(new {ClassName}(remainingIds, this.chainDepth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Handles errors during execution
|
||||
* @param jobId The async job Id
|
||||
* @param e The exception that occurred
|
||||
*/
|
||||
private void handleError(Id jobId, Exception e) {
|
||||
System.debug(LoggingLevel.ERROR,
|
||||
'{ClassName} failed (Job: ' + jobId + '): ' +
|
||||
e.getMessage() + '\n' + e.getStackTraceString()
|
||||
);
|
||||
// TODO: Persist error to a log object or send notification
|
||||
}
|
||||
}
|
||||
75
skills/apex-development/apex-class/templates/schedulable.cls
Normal file
75
skills/apex-development/apex-class/templates/schedulable.cls
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @description Schedulable Apex class for {describe the scheduled operation}.
|
||||
* Delegates heavy processing to a Batch or Queueable job.
|
||||
* Keep execute() lightweight — it should only launch other jobs.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*
|
||||
* @example
|
||||
* // Schedule to run daily at 2 AM
|
||||
* String jobId = System.schedule(
|
||||
* '{ClassName} - Daily',
|
||||
* {ClassName}.CRON_DAILY_2AM,
|
||||
* new {ClassName}()
|
||||
* );
|
||||
*
|
||||
* // Or use the convenience method
|
||||
* String jobId = {ClassName}.scheduleDaily();
|
||||
*/
|
||||
public with sharing class {ClassName} implements Schedulable {
|
||||
|
||||
// ─── CRON Expressions ────────────────────────────────────────────────
|
||||
// Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
|
||||
|
||||
/** @description Runs daily at 2:00 AM */
|
||||
public static final String CRON_DAILY_2AM = '0 0 2 * * ?';
|
||||
|
||||
/** @description Runs every weekday at 6:00 AM */
|
||||
public static final String CRON_WEEKDAYS_6AM = '0 0 6 ? * MON-FRI';
|
||||
|
||||
/** @description Runs hourly at the top of the hour */
|
||||
public static final String CRON_HOURLY = '0 0 * * * ?';
|
||||
|
||||
// ─── Schedulable Interface ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Entry point for the scheduled execution.
|
||||
* Delegates to a Batch or Queueable for the actual work.
|
||||
* @param sc The schedulable context
|
||||
*/
|
||||
public void execute(SchedulableContext sc) {
|
||||
// Option A: Launch a Batch job
|
||||
// Database.executeBatch(new {BatchClassName}(), 200);
|
||||
|
||||
// Option B: Launch a Queueable job
|
||||
// System.enqueueJob(new {QueueableClassName}(params));
|
||||
|
||||
// TODO: Implement delegation to appropriate async job
|
||||
}
|
||||
|
||||
// ─── Convenience Scheduling Methods ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Schedules this job to run daily at 2 AM
|
||||
* @return The scheduled job Id
|
||||
*/
|
||||
public static String scheduleDaily() {
|
||||
return System.schedule(
|
||||
'{ClassName} - Daily 2AM',
|
||||
CRON_DAILY_2AM,
|
||||
new {ClassName}()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Aborts this scheduled job by name
|
||||
* @param jobName The name used when scheduling
|
||||
*/
|
||||
public static void abort(String jobName) {
|
||||
for (CronTrigger ct : [
|
||||
SELECT Id FROM CronTrigger
|
||||
WHERE CronJobDetail.Name = :jobName
|
||||
]) {
|
||||
System.abortJob(ct.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
skills/apex-development/apex-class/templates/selector.cls
Normal file
92
skills/apex-development/apex-class/templates/selector.cls
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @description Selector class for {SObject} queries.
|
||||
* Encapsulates all SOQL for {SObject} records.
|
||||
* All methods return bulkified results (Lists or Maps).
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
public with sharing class {SObject}Selector {
|
||||
|
||||
// ─── Field Lists ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Returns the default set of fields to query for {SObject}.
|
||||
* Centralizes field references to keep queries DRY.
|
||||
* @return Comma-separated field list as a String
|
||||
*/
|
||||
private static String getDefaultFields() {
|
||||
return String.join(
|
||||
new List<String>{
|
||||
'Id',
|
||||
'Name',
|
||||
'CreatedDate',
|
||||
'LastModifiedDate'
|
||||
// TODO: Add additional fields here
|
||||
},
|
||||
', '
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Query Methods ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Selects {SObject} records by their Ids
|
||||
* @param recordIds Set of {SObject} Ids to query
|
||||
* @return List of {SObject} records matching the provided Ids
|
||||
* @example
|
||||
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
|
||||
* List<{SObject}> results = {SObject}Selector.selectByIds(ids);
|
||||
*/
|
||||
public static List<{SObject}> selectByIds(Set<Id> recordIds) {
|
||||
if (recordIds == null || recordIds.isEmpty()) {
|
||||
return new List<{SObject}>();
|
||||
}
|
||||
|
||||
return Database.query(
|
||||
'SELECT ' + getDefaultFields() +
|
||||
' FROM {SObject}' +
|
||||
' WHERE Id IN :recordIds'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects {SObject} records as a Map keyed by Id
|
||||
* @param recordIds Set of {SObject} Ids to query
|
||||
* @return Map of Id to {SObject}
|
||||
*/
|
||||
public static Map<Id, {SObject}> selectMapByIds(Set<Id> recordIds) {
|
||||
return new Map<Id, {SObject}>(selectByIds(recordIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Selects {SObject} records by a specific field value
|
||||
* @param fieldName API name of the field to filter on
|
||||
* @param values Set of values to match
|
||||
* @return List of matching {SObject} records
|
||||
* @example
|
||||
* List<{SObject}> results = {SObject}Selector.selectByField('Status__c', new Set<String>{ 'Active' });
|
||||
*/
|
||||
public static List<{SObject}> selectByField(String fieldName, Set<String> values) {
|
||||
if (String.isBlank(fieldName) || values == null || values.isEmpty()) {
|
||||
return new List<{SObject}>();
|
||||
}
|
||||
|
||||
// Validate field name to prevent SOQL injection
|
||||
Schema.SObjectField field = Schema.SObjectType.{SObject}.fields.getMap().get(fieldName);
|
||||
if (field == null) {
|
||||
throw new QueryException('Invalid field name: ' + fieldName);
|
||||
}
|
||||
|
||||
return Database.query(
|
||||
'SELECT ' + getDefaultFields() +
|
||||
' FROM {SObject}' +
|
||||
' WHERE ' + String.escapeSingleQuotes(fieldName) + ' IN :values'
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Exception ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Custom exception for query errors
|
||||
*/
|
||||
public class QueryException extends Exception {}
|
||||
}
|
||||
69
skills/apex-development/apex-class/templates/service.cls
Normal file
69
skills/apex-development/apex-class/templates/service.cls
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @description Service class for {SObject} business logic.
|
||||
* Follows separation of concerns: delegates queries to {SObject}Selector
|
||||
* and SObject manipulation to {SObject}Domain where applicable.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
public with sharing class {SObject}Service {
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────
|
||||
private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description {Describe the primary operation}
|
||||
* @param recordIds Set of {SObject} Ids to process
|
||||
* @return List of processed {SObject} records
|
||||
* @throws {SObject}ServiceException if processing fails
|
||||
* @example
|
||||
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
|
||||
* List<{SObject}> results = {SObject}Service.process{SObject}s(ids);
|
||||
*/
|
||||
public static List<{SObject}> process{SObject}s(Set<Id> recordIds) {
|
||||
// Guard clause
|
||||
if (recordIds == null || recordIds.isEmpty()) {
|
||||
throw new {SObject}ServiceException(ERROR_NULL_INPUT);
|
||||
}
|
||||
|
||||
// Query via Selector
|
||||
List<{SObject}> records = {SObject}Selector.selectByIds(recordIds);
|
||||
|
||||
// Apply business logic
|
||||
// TODO: Implement business logic here
|
||||
|
||||
// DML
|
||||
try {
|
||||
update records;
|
||||
} catch (DmlException e) {
|
||||
throw new {SObject}ServiceException(
|
||||
'Failed to update {SObject} records: ' + e.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
// ─── Convenience Overloads ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Single-record convenience overload
|
||||
* @param recordId The {SObject} Id to process
|
||||
* @return The processed {SObject} record
|
||||
*/
|
||||
public static {SObject} process{SObject}(Id recordId) {
|
||||
List<{SObject}> results = process{SObject}s(new Set<Id>{ recordId });
|
||||
return results.isEmpty() ? null : results[0];
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────
|
||||
|
||||
// TODO: Add private helper methods as needed
|
||||
|
||||
// ─── Exception ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Custom exception for {SObject}Service errors
|
||||
*/
|
||||
public class {SObject}ServiceException extends Exception {}
|
||||
}
|
||||
97
skills/apex-development/apex-class/templates/utility.cls
Normal file
97
skills/apex-development/apex-class/templates/utility.cls
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @description Utility class for {describe the category of utilities: String, Date, Collection, etc.}.
|
||||
* All methods are static and side-effect-free (no SOQL, no DML).
|
||||
* Private constructor prevents instantiation.
|
||||
* @author Generated by Apex Class Writer Skill
|
||||
*/
|
||||
public with sharing class {ClassName} {
|
||||
|
||||
// ─── Private Constructor ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @description Prevents instantiation — use static methods only
|
||||
*/
|
||||
@TestVisible
|
||||
private {ClassName}() {
|
||||
// Utility class — do not instantiate
|
||||
}
|
||||
|
||||
// ─── Public Methods ──────────────────────────────────────────────────
|
||||
|
||||
// TODO: Add utility methods below. Examples:
|
||||
|
||||
/**
|
||||
* @description Safely converts a String to an Integer, returning a default if parsing fails
|
||||
* @param value The String to parse
|
||||
* @param defaultValue The fallback value if parsing fails
|
||||
* @return The parsed Integer or the default value
|
||||
* @example
|
||||
* Integer result = {ClassName}.safeParseInteger('42', 0); // returns 42
|
||||
* Integer result = {ClassName}.safeParseInteger('abc', 0); // returns 0
|
||||
*/
|
||||
public static Integer safeParseInteger(String value, Integer defaultValue) {
|
||||
if (String.isBlank(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(value.trim());
|
||||
} catch (TypeException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Chunks a list into smaller sublists of the specified size.
|
||||
* Useful for processing records in governor-limit-safe batches.
|
||||
* @param items The list to chunk
|
||||
* @param chunkSize The maximum size of each chunk
|
||||
* @return A list of sublists, each containing up to chunkSize elements
|
||||
* @example
|
||||
* List<List<String>> chunks = {ClassName}.chunkList(myList, 200);
|
||||
*/
|
||||
public static List<List<Object>> chunkList(List<Object> items, Integer chunkSize) {
|
||||
List<List<Object>> chunks = new List<List<Object>>();
|
||||
if (items == null || items.isEmpty() || chunkSize <= 0) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
List<Object> currentChunk = new List<Object>();
|
||||
for (Object item : items) {
|
||||
currentChunk.add(item);
|
||||
if (currentChunk.size() == chunkSize) {
|
||||
chunks.add(currentChunk);
|
||||
currentChunk = new List<Object>();
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentChunk.isEmpty()) {
|
||||
chunks.add(currentChunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Extracts a Set of non-null field values from a list of SObjects
|
||||
* @param records The SObject records to extract from
|
||||
* @param fieldName The API name of the field to extract
|
||||
* @return A Set of non-null String values
|
||||
* @example
|
||||
* Set<String> emails = {ClassName}.pluckStrings(contacts, 'Email');
|
||||
*/
|
||||
public static Set<String> pluckStrings(List<SObject> records, String fieldName) {
|
||||
Set<String> values = new Set<String>();
|
||||
if (records == null || String.isBlank(fieldName)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (SObject record : records) {
|
||||
Object val = record.get(fieldName);
|
||||
if (val != null) {
|
||||
values.add(String.valueOf(val));
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user