diff --git a/skills/apex-development/apex-class/SKILL.md b/skills/apex-development/apex-class/SKILL.md new file mode 100644 index 0000000..4041d5e --- /dev/null +++ b/skills/apex-development/apex-class/SKILL.md @@ -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` 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 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 + + + {API_VERSION} + Active + +``` + +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` or `Map` +- 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` — 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` 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 diff --git a/skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls b/skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls new file mode 100644 index 0000000..30ba97d --- /dev/null +++ b/skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls @@ -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, 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 errorMessages = new List(); + + // ─── 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 scope) { + totalScanned += scope.size(); + + // Build duplicate keys for this batch + Map> dupeKeyMap = new Map>(); + for (Account acct : scope) { + String key = buildDuplicateKey(acct); + if (String.isNotBlank(key)) { + if (!dupeKeyMap.containsKey(key)) { + dupeKeyMap.put(key, new List()); + } + dupeKeyMap.get(key).add(acct); + } + } + + // Flag records that share a key with another record + List toUpdate = new List(); + for (String key : dupeKeyMap.keySet()) { + List 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 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{ 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 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); + } +} diff --git a/skills/apex-development/apex-class/examples/AccountSelector.cls b/skills/apex-development/apex-class/examples/AccountSelector.cls new file mode 100644 index 0000000..990f043 --- /dev/null +++ b/skills/apex-development/apex-class/examples/AccountSelector.cls @@ -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{ + '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{ + '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 ids = new Set{ '001xx000003DGbY' }; + * List results = AccountSelector.selectByIds(ids); + */ + public static List selectByIds(Set recordIds) { + if (recordIds == null || recordIds.isEmpty()) { + return new List(); + } + + 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 selectMapByIds(Set recordIds) { + return new Map(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 selectWithBillingAddress(Set recordIds) { + if (recordIds == null || recordIds.isEmpty()) { + return new List(); + } + + 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 prospects = AccountSelector.selectByType( + * new Set{ 'Prospect', 'Customer - Direct' } + * ); + */ + public static List selectByType(Set accountTypes) { + if (accountTypes == null || accountTypes.isEmpty()) { + return new List(); + } + + 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 techAccounts = AccountSelector.selectByIndustryAndRevenue( + * new Set{ 'Technology' }, + * 1000000 + * ); + */ + public static List selectByIndustryAndRevenue(Set industries, Decimal minRevenue) { + if (industries == null || industries.isEmpty()) { + return new List(); + } + + 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 selectWithContacts(Set recordIds) { + if (recordIds == null || recordIds.isEmpty()) { + return new List(); + } + + 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 results = AccountSelector.countByIndustry(); + * for (AggregateResult ar : results) { + * System.debug(ar.get('Industry') + ': ' + ar.get('cnt')); + * } + */ + public static List countByIndustry() { + return [ + SELECT Industry, COUNT(Id) cnt + FROM Account + WHERE Industry != NULL + GROUP BY Industry + ORDER BY COUNT(Id) DESC + ]; + } +} diff --git a/skills/apex-development/apex-class/examples/AccountService.cls b/skills/apex-development/apex-class/examples/AccountService.cls new file mode 100644 index 0000000..eafee0a --- /dev/null +++ b/skills/apex-development/apex-class/examples/AccountService.cls @@ -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> mergeMap = new Map>{ + * masterAcctId => new Set{ dupeId1, dupeId2 } + * }; + * List mergedIds = AccountService.mergeAccounts(mergeMap); + */ + public static List mergeAccounts(Map> mergeMap) { + if (mergeMap == null || mergeMap.isEmpty()) { + throw new AccountServiceException(ERROR_NULL_INPUT); + } + + // Collect all Ids for a single query + Set allIds = new Set(); + allIds.addAll(mergeMap.keySet()); + for (Set dupeIds : mergeMap.values()) { + allIds.addAll(dupeIds); + } + + // Query all records at once via Selector + Map accountMap = AccountSelector.selectMapByIds(allIds); + + List successfulMerges = new List(); + List errors = new List(); + + for (Id masterId : mergeMap.keySet()) { + Account master = accountMap.get(masterId); + if (master == null) { + errors.add('Master account not found: ' + masterId); + continue; + } + + List duplicates = new List(); + 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 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 accountIds) { + if (accountIds == null || accountIds.isEmpty()) { + throw new AccountServiceException(ERROR_NULL_INPUT); + } + + List accounts = AccountSelector.selectWithBillingAddress(accountIds); + Map territoryMap = loadTerritoryMappings(); + + List toUpdate = new List(); + 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 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{ accountId }); + } + + // ─── Private Helpers ───────────────────────────────────────────────── + + /** + * @description Loads territory mappings from Custom Metadata + * @return Map of territory key (State:Country) to territory name + */ + private static Map loadTerritoryMappings() { + Map mappings = new Map(); + 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> chunkAccounts(List accounts, Integer chunkSize) { + List> chunks = new List>(); + List current = new List(); + + for (Account acct : accounts) { + current.add(acct); + if (current.size() == chunkSize) { + chunks.add(current); + current = new List(); + } + } + 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 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 {} +} diff --git a/skills/apex-development/apex-class/templates/abstract.cls b/skills/apex-development/apex-class/templates/abstract.cls new file mode 100644 index 0000000..cf2716a --- /dev/null +++ b/skills/apex-development/apex-class/templates/abstract.cls @@ -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 getHeaders() { + * return new Map{ + * '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 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 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 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 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 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 {} +} diff --git a/skills/apex-development/apex-class/templates/batch.cls b/skills/apex-development/apex-class/templates/batch.cls new file mode 100644 index 0000000..07807d5 --- /dev/null +++ b/skills/apex-development/apex-class/templates/batch.cls @@ -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, Database.Stateful { + + // ─── Constants ─────────────────────────────────────────────────────── + private static final Integer DEFAULT_BATCH_SIZE = 200; + + // ─── Stateful Tracking ─────────────────────────────────────────────── + private Integer totalProcessed = 0; + private Integer totalErrors = 0; + private List errorMessages = new List(); + + // ─── 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 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{ + {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 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); + } +} diff --git a/skills/apex-development/apex-class/templates/domain.cls b/skills/apex-development/apex-class/templates/domain.cls new file mode 100644 index 0000000..e32615a --- /dev/null +++ b/skills/apex-development/apex-class/templates/domain.cls @@ -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 getChangedFields({SObject} oldRecord, {SObject} newRecord) { + Set changedFields = new Set(); + + if (oldRecord == null || newRecord == null) { + return changedFields; + } + + Map 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 +} diff --git a/skills/apex-development/apex-class/templates/dto.cls b/skills/apex-development/apex-class/templates/dto.cls new file mode 100644 index 0000000..7477663 --- /dev/null +++ b/skills/apex-development/apex-class/templates/dto.cls @@ -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 tags { get; set; } + + // TODO: Add additional properties as needed + + // ─── Constructors ──────────────────────────────────────────────────── + + /** + * @description No-arg constructor for deserialization compatibility + */ + public {ClassName}() { + this.tags = new List(); + 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 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); + } +} diff --git a/skills/apex-development/apex-class/templates/exception.cls b/skills/apex-development/apex-class/templates/exception.cls new file mode 100644 index 0000000..1d14d94 --- /dev/null +++ b/skills/apex-development/apex-class/templates/exception.cls @@ -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 failedRecordIds; + // public String detail; + // + // public {ClassName}Detail(String errorCode, List failedRecordIds, String detail) { + // this.errorCode = errorCode; + // this.failedRecordIds = failedRecordIds; + // this.detail = detail; + // } + // + // public override String toString() { + // return '[' + errorCode + '] ' + detail + ' (Records: ' + failedRecordIds + ')'; + // } + // } +} diff --git a/skills/apex-development/apex-class/templates/interface.cls b/skills/apex-development/apex-class/templates/interface.cls new file mode 100644 index 0000000..fc1da63 --- /dev/null +++ b/skills/apex-development/apex-class/templates/interface.cls @@ -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 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 params); + // Boolean isEligible(SObject record); + // List process(List records); +} diff --git a/skills/apex-development/apex-class/templates/queueable.cls b/skills/apex-development/apex-class/templates/queueable.cls new file mode 100644 index 0000000..eaa224a --- /dev/null +++ b/skills/apex-development/apex-class/templates/queueable.cls @@ -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 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 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 recordIds, Integer chainDepth) { + this.recordIds = recordIds ?? new Set(); + 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 remainingIds = new Set(); + + 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 + } +} diff --git a/skills/apex-development/apex-class/templates/schedulable.cls b/skills/apex-development/apex-class/templates/schedulable.cls new file mode 100644 index 0000000..1e9f932 --- /dev/null +++ b/skills/apex-development/apex-class/templates/schedulable.cls @@ -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); + } + } +} diff --git a/skills/apex-development/apex-class/templates/selector.cls b/skills/apex-development/apex-class/templates/selector.cls new file mode 100644 index 0000000..3512465 --- /dev/null +++ b/skills/apex-development/apex-class/templates/selector.cls @@ -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{ + '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 ids = new Set{ '001xx000003DGbY' }; + * List<{SObject}> results = {SObject}Selector.selectByIds(ids); + */ + public static List<{SObject}> selectByIds(Set 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 selectMapByIds(Set recordIds) { + return new Map(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{ 'Active' }); + */ + public static List<{SObject}> selectByField(String fieldName, Set 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 {} +} diff --git a/skills/apex-development/apex-class/templates/service.cls b/skills/apex-development/apex-class/templates/service.cls new file mode 100644 index 0000000..5f3a6c3 --- /dev/null +++ b/skills/apex-development/apex-class/templates/service.cls @@ -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 ids = new Set{ '001xx000003DGbY' }; + * List<{SObject}> results = {SObject}Service.process{SObject}s(ids); + */ + public static List<{SObject}> process{SObject}s(Set 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{ 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 {} +} diff --git a/skills/apex-development/apex-class/templates/utility.cls b/skills/apex-development/apex-class/templates/utility.cls new file mode 100644 index 0000000..9f3f9fc --- /dev/null +++ b/skills/apex-development/apex-class/templates/utility.cls @@ -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> chunks = {ClassName}.chunkList(myList, 200); + */ + public static List> chunkList(List items, Integer chunkSize) { + List> chunks = new List>(); + if (items == null || items.isEmpty() || chunkSize <= 0) { + return chunks; + } + + List currentChunk = new List(); + for (Object item : items) { + currentChunk.add(item); + if (currentChunk.size() == chunkSize) { + chunks.add(currentChunk); + currentChunk = new List(); + } + } + + 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 emails = {ClassName}.pluckStrings(contacts, 'Email'); + */ + public static Set pluckStrings(List records, String fieldName) { + Set values = new Set(); + 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; + } +}