@salesforce/afv-skills 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +16 -415
  2. package/package.json +5 -3
  3. package/skills/building-ui-bundle-app/SKILL.md +325 -0
  4. package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
  5. package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
  6. package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
  7. package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
  8. package/skills/deploying-ui-bundle/SKILL.md +77 -0
  9. package/skills/generating-apex/CREDITS.md +30 -0
  10. package/skills/generating-apex/SKILL.md +399 -0
  11. package/skills/generating-apex/assets/abstract.cls +132 -0
  12. package/skills/generating-apex/assets/batch.cls +125 -0
  13. package/skills/generating-apex/assets/domain.cls +102 -0
  14. package/skills/generating-apex/assets/dto.cls +108 -0
  15. package/skills/generating-apex/assets/exception.cls +51 -0
  16. package/skills/generating-apex/assets/interface.cls +25 -0
  17. package/skills/generating-apex/assets/invocable.cls +115 -0
  18. package/skills/generating-apex/assets/queueable.cls +92 -0
  19. package/skills/generating-apex/assets/rest-resource.cls +300 -0
  20. package/skills/generating-apex/assets/schedulable.cls +75 -0
  21. package/skills/generating-apex/assets/selector.cls +92 -0
  22. package/skills/generating-apex/assets/service.cls +69 -0
  23. package/skills/generating-apex/assets/trigger.cls +45 -0
  24. package/skills/generating-apex/assets/utility.cls +97 -0
  25. package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
  26. package/skills/generating-apex/references/AccountSelector.cls +193 -0
  27. package/skills/generating-apex/references/AccountService.cls +201 -0
  28. package/skills/generating-apex-test/CREDITS.md +30 -0
  29. package/skills/generating-apex-test/SKILL.md +199 -0
  30. package/skills/generating-apex-test/assets/test-class-template.cls +93 -0
  31. package/skills/generating-apex-test/assets/test-data-factory-template.cls +111 -0
  32. package/skills/generating-apex-test/references/assertion-patterns.md +108 -0
  33. package/skills/generating-apex-test/references/async-testing.md +193 -0
  34. package/skills/generating-apex-test/references/mocking-patterns.md +220 -0
  35. package/skills/generating-apex-test/references/test-data-factory.md +75 -0
  36. package/skills/generating-experience-react-site/SKILL.md +20 -9
  37. package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
  38. package/skills/generating-flexipage/SKILL.md +58 -60
  39. package/skills/generating-ui-bundle-features/SKILL.md +45 -0
  40. package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
  41. package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
  42. package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
  43. package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
  44. package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
  45. package/skills/searching-media/SKILL.md +342 -0
  46. package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
  47. package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
  48. package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
  49. package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
  50. package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
  51. package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
  52. package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
  53. package/skills/building-webapp-data-visualization/SKILL.md +0 -72
  54. package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
  55. package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
  56. package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
  57. package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
  58. package/skills/building-webapp-react-components/SKILL.md +0 -96
  59. package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
  60. package/skills/configuring-webapp-metadata/SKILL.md +0 -158
  61. package/skills/creating-webapp/SKILL.md +0 -140
  62. package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
  63. package/skills/installing-webapp-features/SKILL.md +0 -210
  64. /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
  65. /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
  66. /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
  67. /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
  68. /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/troubleshooting.md +0 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Queueable Apex class for {describe the async operation}.
3
+ * Accepts data through the constructor for stateful processing.
4
+ * Optionally implements Database.AllowsCallouts for external integrations.
5
+ * @author Generated by Apex Class Writer Skill
6
+ *
7
+ * @example
8
+ * // Enqueue the job
9
+ * Id jobId = System.enqueueJob(new {ClassName}(recordIds));
10
+ */
11
+ public with sharing class {ClassName} implements Queueable /*, Database.AllowsCallouts */ {
12
+
13
+ // ─── Constants ───────────────────────────────────────────────────────
14
+ private static final Integer MAX_CHAIN_DEPTH = 5;
15
+
16
+ // ─── Instance Variables (Stateful) ───────────────────────────────────
17
+ private Set<Id> recordIds;
18
+ private Integer chainDepth;
19
+
20
+ // ─── Constructors ────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Creates a new queueable job to process the specified records
24
+ * @param recordIds Set of record Ids to process
25
+ */
26
+ public {ClassName}(Set<Id> recordIds) {
27
+ this(recordIds, 0);
28
+ }
29
+
30
+ /**
31
+ * Creates a new queueable job with chain depth tracking
32
+ * @param recordIds Set of record Ids to process
33
+ * @param chainDepth Current depth in the queueable chain
34
+ */
35
+ public {ClassName}(Set<Id> recordIds, Integer chainDepth) {
36
+ this.recordIds = recordIds ?? new Set<Id>();
37
+ this.chainDepth = chainDepth;
38
+ }
39
+
40
+ // ─── Queueable Interface ─────────────────────────────────────────────
41
+
42
+ /**
43
+ * Executes the asynchronous work
44
+ * @param context The queueable context
45
+ */
46
+ public void execute(QueueableContext context) {
47
+ if (this.recordIds.isEmpty()) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ // TODO: Implement the async processing logic
53
+ // List<{SObject}> records = {SObject}Selector.selectByIds(this.recordIds);
54
+ // ... process records ...
55
+
56
+ // Chain to next job if there's more work and we haven't hit the depth limit
57
+ chainIfNeeded();
58
+
59
+ } catch (Exception e) {
60
+ handleError(context.getJobId(), e);
61
+ }
62
+ }
63
+
64
+ // ─── Private Helpers ─────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Chains to the next queueable job if needed, with depth guard
68
+ */
69
+ private void chainIfNeeded() {
70
+ // TODO: Determine if chaining is needed (e.g., remaining records to process)
71
+ Set<Id> remainingIds = new Set<Id>();
72
+
73
+ if (!remainingIds.isEmpty() && this.chainDepth < MAX_CHAIN_DEPTH) {
74
+ if (!Test.isRunningTest()) {
75
+ System.enqueueJob(new {ClassName}(remainingIds, this.chainDepth + 1));
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Handles errors during execution
82
+ * @param jobId The async job Id
83
+ * @param e The exception that occurred
84
+ */
85
+ private void handleError(Id jobId, Exception e) {
86
+ System.debug(LoggingLevel.ERROR,
87
+ '{ClassName} failed (Job: ' + jobId + '): ' +
88
+ e.getMessage() + '\n' + e.getStackTraceString()
89
+ );
90
+ // TODO: Persist error to a log object or send notification
91
+ }
92
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * REST API endpoint for {SObject} operations.
3
+ * Exposes CRUD operations via @RestResource at /services/apexrest/{urlPath}/v1/*.
4
+ * Uses versioned URL mapping for future-proof API evolution.
5
+ * All queries use WITH USER_MODE for CRUD/FLS enforcement.
6
+ *
7
+ * Authentication: OAuth 2.0 via Connected App
8
+ * Base URL: /services/apexrest/{urlPath}/v1/
9
+ */
10
+ @RestResource(urlMapping='/{urlPath}/v1/*')
11
+ global with sharing class {ClassName} {
12
+
13
+ // ─── Constants ────────────────────────────────────────────────────────
14
+ private static final Integer DEFAULT_PAGE_SIZE = 20;
15
+ private static final Integer MAX_PAGE_SIZE = 200;
16
+ private static final String ERROR_MISSING_ID = 'Record Id is required in the URL path.';
17
+ private static final String ERROR_INVALID_ID = 'Invalid Salesforce Id format.';
18
+ private static final String ERROR_MISSING_BODY = 'Request body is required.';
19
+ private static final String ERROR_NOT_FOUND = '{SObject} record not found.';
20
+ private static final String ERROR_INSUFFICIENT_ACCESS = 'Insufficient access to perform this operation.';
21
+
22
+ // ─── GET — Retrieve ───────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Retrieves a single {SObject} by Id (URL path) or a paginated list (query params).
26
+ * Single: GET /services/apexrest/{urlPath}/v1/{recordId}
27
+ * List: GET /services/apexrest/{urlPath}/v1?pageSize=20&offset=0
28
+ * @return ApiResponse containing the requested data
29
+ */
30
+ @HttpGet
31
+ global static ApiResponse doGet() {
32
+ RestRequest req = RestContext.request;
33
+ RestResponse res = RestContext.response;
34
+
35
+ try {
36
+ String recordId = extractIdFromUri(req.requestURI);
37
+
38
+ if (String.isNotBlank(recordId)) {
39
+ return getSingleRecord(recordId, res);
40
+ }
41
+ return getRecordList(req, res);
42
+
43
+ } catch (Exception e) {
44
+ return handleError(res, 500, e.getMessage());
45
+ }
46
+ }
47
+
48
+ // ─── POST — Create ───────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Creates a new {SObject} record from the JSON request body.
52
+ * POST /services/apexrest/{urlPath}/v1/
53
+ * @return ApiResponse with the created record Id
54
+ */
55
+ @HttpPost
56
+ global static ApiResponse doPost() {
57
+ RestRequest req = RestContext.request;
58
+ RestResponse res = RestContext.response;
59
+
60
+ try {
61
+ if (req.requestBody == null || String.isBlank(req.requestBody.toString())) {
62
+ return handleError(res, 400, ERROR_MISSING_BODY);
63
+ }
64
+
65
+ {ClassName}Request payload = ({ClassName}Request) JSON.deserialize(
66
+ req.requestBody.toString(),
67
+ {ClassName}Request.class
68
+ );
69
+
70
+ // TODO: Map request payload to SObject fields
71
+ {SObject} record = new {SObject}(
72
+ Name = payload.name
73
+ );
74
+
75
+ Database.SaveResult saveResult = Database.insert(record, true);
76
+ if (saveResult.isSuccess()) {
77
+ res.statusCode = 201;
78
+ return new ApiResponse(true, 'Record created successfully.', record.Id);
79
+ }
80
+
81
+ return handleError(res, 422, saveResult.getErrors()[0].getMessage());
82
+
83
+ } catch (JSONException e) {
84
+ return handleError(res, 400, 'Malformed JSON: ' + e.getMessage());
85
+ } catch (DmlException e) {
86
+ return handleError(res, 422, 'Validation failed: ' + e.getDmlMessage(0));
87
+ } catch (Exception e) {
88
+ return handleError(res, 500, e.getMessage());
89
+ }
90
+ }
91
+
92
+ // ─── PATCH — Partial Update ───────────────────────────────────────────
93
+
94
+ /**
95
+ * Partially updates an existing {SObject} record.
96
+ * PATCH /services/apexrest/{urlPath}/v1/{recordId}
97
+ * @return ApiResponse confirming the update
98
+ */
99
+ @HttpPatch
100
+ global static ApiResponse doPatch() {
101
+ RestRequest req = RestContext.request;
102
+ RestResponse res = RestContext.response;
103
+
104
+ try {
105
+ String recordId = extractIdFromUri(req.requestURI);
106
+ if (String.isBlank(recordId)) {
107
+ return handleError(res, 400, ERROR_MISSING_ID);
108
+ }
109
+ if (!isValidSalesforceId(recordId)) {
110
+ return handleError(res, 400, ERROR_INVALID_ID);
111
+ }
112
+ if (req.requestBody == null || String.isBlank(req.requestBody.toString())) {
113
+ return handleError(res, 400, ERROR_MISSING_BODY);
114
+ }
115
+
116
+ List<{SObject}> existing = [
117
+ SELECT Id FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1
118
+ ];
119
+ if (existing.isEmpty()) {
120
+ return handleError(res, 404, ERROR_NOT_FOUND);
121
+ }
122
+
123
+ Map<String, Object> fieldUpdates = (Map<String, Object>) JSON.deserializeUntyped(
124
+ req.requestBody.toString()
125
+ );
126
+
127
+ {SObject} record = existing[0];
128
+ // TODO: Apply allowed field updates from the payload to the record
129
+ // for (String fieldName : fieldUpdates.keySet()) {
130
+ // record.put(fieldName, fieldUpdates.get(fieldName));
131
+ // }
132
+
133
+ Database.SaveResult saveResult = Database.update(record, true);
134
+ if (saveResult.isSuccess()) {
135
+ res.statusCode = 200;
136
+ return new ApiResponse(true, 'Record updated successfully.', record.Id);
137
+ }
138
+
139
+ return handleError(res, 422, saveResult.getErrors()[0].getMessage());
140
+
141
+ } catch (JSONException e) {
142
+ return handleError(res, 400, 'Malformed JSON: ' + e.getMessage());
143
+ } catch (DmlException e) {
144
+ return handleError(res, 422, 'Validation failed: ' + e.getDmlMessage(0));
145
+ } catch (Exception e) {
146
+ return handleError(res, 500, e.getMessage());
147
+ }
148
+ }
149
+
150
+ // ─── DELETE — Remove ──────────────────────────────────────────────────
151
+
152
+ /**
153
+ * Deletes a {SObject} record by Id.
154
+ * DELETE /services/apexrest/{urlPath}/v1/{recordId}
155
+ * @return ApiResponse confirming the deletion
156
+ */
157
+ @HttpDelete
158
+ global static ApiResponse doDelete() {
159
+ RestRequest req = RestContext.request;
160
+ RestResponse res = RestContext.response;
161
+
162
+ try {
163
+ String recordId = extractIdFromUri(req.requestURI);
164
+ if (String.isBlank(recordId)) {
165
+ return handleError(res, 400, ERROR_MISSING_ID);
166
+ }
167
+ if (!isValidSalesforceId(recordId)) {
168
+ return handleError(res, 400, ERROR_INVALID_ID);
169
+ }
170
+
171
+ List<{SObject}> existing = [
172
+ SELECT Id FROM {SObject} WHERE Id = :recordId WITH USER_MODE LIMIT 1
173
+ ];
174
+ if (existing.isEmpty()) {
175
+ return handleError(res, 404, ERROR_NOT_FOUND);
176
+ }
177
+
178
+ Database.DeleteResult deleteResult = Database.delete(existing[0], true);
179
+ if (deleteResult.isSuccess()) {
180
+ res.statusCode = 200;
181
+ return new ApiResponse(true, 'Record deleted successfully.', recordId);
182
+ }
183
+
184
+ return handleError(res, 422, deleteResult.getErrors()[0].getMessage());
185
+
186
+ } catch (DmlException e) {
187
+ return handleError(res, 422, e.getDmlMessage(0));
188
+ } catch (Exception e) {
189
+ return handleError(res, 500, e.getMessage());
190
+ }
191
+ }
192
+
193
+ // ─── Private Helpers ──────────────────────────────────────────────────
194
+
195
+ private static ApiResponse getSingleRecord(String recordId, RestResponse res) {
196
+ if (!isValidSalesforceId(recordId)) {
197
+ return handleError(res, 400, ERROR_INVALID_ID);
198
+ }
199
+
200
+ List<{SObject}> records = [
201
+ SELECT Id, Name, CreatedDate, LastModifiedDate
202
+ FROM {SObject}
203
+ WHERE Id = :recordId
204
+ WITH USER_MODE
205
+ LIMIT 1
206
+ ];
207
+
208
+ if (records.isEmpty()) {
209
+ return handleError(res, 404, ERROR_NOT_FOUND);
210
+ }
211
+
212
+ res.statusCode = 200;
213
+ ApiResponse response = new ApiResponse(true, 'Record retrieved successfully.', recordId);
214
+ response.data = records[0];
215
+ return response;
216
+ }
217
+
218
+ private static ApiResponse getRecordList(RestRequest req, RestResponse res) {
219
+ Integer pageSize = getIntParam(req, 'pageSize', DEFAULT_PAGE_SIZE);
220
+ Integer offset = getIntParam(req, 'offset', 0);
221
+
222
+ pageSize = Math.min(pageSize, MAX_PAGE_SIZE);
223
+
224
+ List<{SObject}> records = [
225
+ SELECT Id, Name, CreatedDate, LastModifiedDate
226
+ FROM {SObject}
227
+ WITH USER_MODE
228
+ ORDER BY Name ASC
229
+ LIMIT :pageSize
230
+ OFFSET :offset
231
+ ];
232
+
233
+ res.statusCode = 200;
234
+ ApiResponse response = new ApiResponse(true, 'Records retrieved successfully.', null);
235
+ response.records = records;
236
+ response.pageSize = pageSize;
237
+ response.offset = offset;
238
+ return response;
239
+ }
240
+
241
+ private static String extractIdFromUri(String uri) {
242
+ String lastSegment = uri.substringAfterLast('/');
243
+ if (String.isBlank(lastSegment) || lastSegment == 'v1') {
244
+ return null;
245
+ }
246
+ return lastSegment;
247
+ }
248
+
249
+ private static Boolean isValidSalesforceId(String idValue) {
250
+ return Pattern.matches('[a-zA-Z0-9]{15,18}', idValue);
251
+ }
252
+
253
+ private static Integer getIntParam(RestRequest req, String paramName, Integer defaultValue) {
254
+ String paramValue = req.params.get(paramName);
255
+ if (String.isBlank(paramValue)) {
256
+ return defaultValue;
257
+ }
258
+ try {
259
+ return Integer.valueOf(paramValue);
260
+ } catch (TypeException e) {
261
+ return defaultValue;
262
+ }
263
+ }
264
+
265
+ private static ApiResponse handleError(RestResponse res, Integer statusCode, String message) {
266
+ res.statusCode = statusCode;
267
+ return new ApiResponse(false, message, null);
268
+ }
269
+
270
+ // ─── Request / Response DTOs ──────────────────────────────────────────
271
+
272
+ /**
273
+ * Inbound request payload for POST operations.
274
+ * Extend with additional fields as needed.
275
+ */
276
+ global class {ClassName}Request {
277
+ global String name;
278
+ // TODO: Add fields matching the expected JSON request body
279
+ }
280
+
281
+ /**
282
+ * Standardized API response envelope.
283
+ * All endpoints return this structure for consistent client parsing.
284
+ */
285
+ global class ApiResponse {
286
+ global Boolean success;
287
+ global String message;
288
+ global String recordId;
289
+ global SObject data;
290
+ global List<SObject> records;
291
+ global Integer pageSize;
292
+ global Integer offset;
293
+
294
+ global ApiResponse(Boolean success, String message, String recordId) {
295
+ this.success = success;
296
+ this.message = message;
297
+ this.recordId = recordId;
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Schedulable Apex class for {describe the scheduled operation}.
3
+ * Delegates heavy processing to a Batch or Queueable job.
4
+ * Keep execute() lightweight — it should only launch other jobs.
5
+ * @author Generated by Apex Class Writer Skill
6
+ *
7
+ * @example
8
+ * // Schedule to run daily at 2 AM
9
+ * String jobId = System.schedule(
10
+ * '{ClassName} - Daily',
11
+ * {ClassName}.CRON_DAILY_2AM,
12
+ * new {ClassName}()
13
+ * );
14
+ *
15
+ * // Or use the convenience method
16
+ * String jobId = {ClassName}.scheduleDaily();
17
+ */
18
+ public with sharing class {ClassName} implements Schedulable {
19
+
20
+ // ─── CRON Expressions ────────────────────────────────────────────────
21
+ // Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
22
+
23
+ /** Runs daily at 2:00 AM */
24
+ public static final String CRON_DAILY_2AM = '0 0 2 * * ?';
25
+
26
+ /** Runs every weekday at 6:00 AM */
27
+ public static final String CRON_WEEKDAYS_6AM = '0 0 6 ? * MON-FRI';
28
+
29
+ /** Runs hourly at the top of the hour */
30
+ public static final String CRON_HOURLY = '0 0 * * * ?';
31
+
32
+ // ─── Schedulable Interface ───────────────────────────────────────────
33
+
34
+ /**
35
+ * Entry point for the scheduled execution.
36
+ * Delegates to a Batch or Queueable for the actual work.
37
+ * @param sc The schedulable context
38
+ */
39
+ public void execute(SchedulableContext sc) {
40
+ // Option A: Launch a Batch job
41
+ // Database.executeBatch(new {BatchClassName}(), 200);
42
+
43
+ // Option B: Launch a Queueable job
44
+ // System.enqueueJob(new {QueueableClassName}(params));
45
+
46
+ // TODO: Implement delegation to appropriate async job
47
+ }
48
+
49
+ // ─── Convenience Scheduling Methods ──────────────────────────────────
50
+
51
+ /**
52
+ * Schedules this job to run daily at 2 AM
53
+ * @return The scheduled job Id
54
+ */
55
+ public static String scheduleDaily() {
56
+ return System.schedule(
57
+ '{ClassName} - Daily 2AM',
58
+ CRON_DAILY_2AM,
59
+ new {ClassName}()
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Aborts this scheduled job by name
65
+ * @param jobName The name used when scheduling
66
+ */
67
+ public static void abort(String jobName) {
68
+ for (CronTrigger ct : [
69
+ SELECT Id FROM CronTrigger
70
+ WHERE CronJobDetail.Name = :jobName
71
+ ]) {
72
+ System.abortJob(ct.Id);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Selector class for {SObject} queries.
3
+ * Encapsulates all SOQL for {SObject} records.
4
+ * All methods return bulkified results (Lists or Maps).
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public inherited sharing class {SObject}Selector {
8
+
9
+ // ─── Field Lists ─────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Returns the default set of fields to query for {SObject}.
13
+ * Centralizes field references to keep queries DRY.
14
+ * @return Comma-separated field list as a String
15
+ */
16
+ private static String getDefaultFields() {
17
+ return String.join(
18
+ new List<String>{
19
+ 'Id',
20
+ 'Name',
21
+ 'CreatedDate',
22
+ 'LastModifiedDate'
23
+ // TODO: Add additional fields here
24
+ },
25
+ ', '
26
+ );
27
+ }
28
+
29
+ // ─── Query Methods ───────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Selects {SObject} records by their Ids
33
+ * @param recordIds Set of {SObject} Ids to query
34
+ * @return List of {SObject} records matching the provided Ids
35
+ * @example
36
+ * Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
37
+ * List<{SObject}> results = {SObject}Selector.selectByIds(ids);
38
+ */
39
+ public static List<{SObject}> selectByIds(Set<Id> recordIds) {
40
+ if (recordIds == null || recordIds.isEmpty()) {
41
+ return new List<{SObject}>();
42
+ }
43
+
44
+ return Database.query(
45
+ 'SELECT ' + getDefaultFields() +
46
+ ' FROM {SObject}' +
47
+ ' WHERE Id IN :recordIds'
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Selects {SObject} records as a Map keyed by Id
53
+ * @param recordIds Set of {SObject} Ids to query
54
+ * @return Map of Id to {SObject}
55
+ */
56
+ public static Map<Id, {SObject}> selectMapByIds(Set<Id> recordIds) {
57
+ return new Map<Id, {SObject}>(selectByIds(recordIds));
58
+ }
59
+
60
+ /**
61
+ * Selects {SObject} records by a specific field value
62
+ * @param fieldName API name of the field to filter on
63
+ * @param values Set of values to match
64
+ * @return List of matching {SObject} records
65
+ * @example
66
+ * List<{SObject}> results = {SObject}Selector.selectByField('Status__c', new Set<String>{ 'Active' });
67
+ */
68
+ public static List<{SObject}> selectByField(String fieldName, Set<String> values) {
69
+ if (String.isBlank(fieldName) || values == null || values.isEmpty()) {
70
+ return new List<{SObject}>();
71
+ }
72
+
73
+ // Validate field name to prevent SOQL injection
74
+ Schema.SObjectField field = Schema.SObjectType.{SObject}.fields.getMap().get(fieldName);
75
+ if (field == null) {
76
+ throw new QueryException('Invalid field name: ' + fieldName);
77
+ }
78
+
79
+ return Database.query(
80
+ 'SELECT ' + getDefaultFields() +
81
+ ' FROM {SObject}' +
82
+ ' WHERE ' + String.escapeSingleQuotes(fieldName) + ' IN :values'
83
+ );
84
+ }
85
+
86
+ // ─── Exception ───────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Custom exception for query errors
90
+ */
91
+ public class QueryException extends Exception {}
92
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Service class for {SObject} business logic.
3
+ * Follows separation of concerns: delegates queries to {SObject}Selector
4
+ * and SObject manipulation to {SObject}Domain where applicable.
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public with sharing class {SObject}Service {
8
+
9
+ // ─── Constants ───────────────────────────────────────────────────────
10
+ private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
11
+
12
+ // ─── Public API ──────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * {Describe the primary operation}
16
+ * @param recordIds Set of {SObject} Ids to process
17
+ * @return List of processed {SObject} records
18
+ * @throws {SObject}ServiceException if processing fails
19
+ * @example
20
+ * Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
21
+ * List<{SObject}> results = {SObject}Service.process{SObject}s(ids);
22
+ */
23
+ public static List<{SObject}> process{SObject}s(Set<Id> recordIds) {
24
+ // Guard clause
25
+ if (recordIds == null || recordIds.isEmpty()) {
26
+ throw new {SObject}ServiceException(ERROR_NULL_INPUT);
27
+ }
28
+
29
+ // Query via Selector
30
+ List<{SObject}> records = {SObject}Selector.selectByIds(recordIds);
31
+
32
+ // Apply business logic
33
+ // TODO: Implement business logic here
34
+
35
+ // DML
36
+ try {
37
+ update records;
38
+ } catch (DmlException e) {
39
+ throw new {SObject}ServiceException(
40
+ 'Failed to update {SObject} records: ' + e.getMessage()
41
+ );
42
+ }
43
+
44
+ return records;
45
+ }
46
+
47
+ // ─── Convenience Overloads ───────────────────────────────────────────
48
+
49
+ /**
50
+ * Single-record convenience overload
51
+ * @param recordId The {SObject} Id to process
52
+ * @return The processed {SObject} record
53
+ */
54
+ public static {SObject} process{SObject}(Id recordId) {
55
+ List<{SObject}> results = process{SObject}s(new Set<Id>{ recordId });
56
+ return results.isEmpty() ? null : results[0];
57
+ }
58
+
59
+ // ─── Private Helpers ─────────────────────────────────────────────────
60
+
61
+ // TODO: Add private helper methods as needed
62
+
63
+ // ─── Exception ───────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Custom exception for {SObject}Service errors
67
+ */
68
+ public class {SObject}ServiceException extends Exception {}
69
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @author Generated by Apex Class Writer Skill
3
+ */
4
+ trigger {SObject}Trigger on {SObject} (
5
+ before insert,
6
+ before update,
7
+ before delete,
8
+ after insert,
9
+ after update,
10
+ after delete,
11
+ after undelete
12
+ ) {
13
+ // ─── Option A: Trigger Actions Framework (TAF) ───────────────────────
14
+ // Delegates to Trigger_Action__mdt-registered action classes.
15
+ // Each action class handles one concern in one context.
16
+ //
17
+ // new MetadataTriggerHandler().run();
18
+
19
+ // ─── Option B: Custom Handler Pattern ────────────────────────────────
20
+ // Delegates to a single handler class that routes by context.
21
+ //
22
+ // {SObject}TriggerHandler handler = new {SObject}TriggerHandler();
23
+ //
24
+ // if (Trigger.isBefore) {
25
+ // if (Trigger.isInsert) {
26
+ // handler.beforeInsert(Trigger.new);
27
+ // } else if (Trigger.isUpdate) {
28
+ // handler.beforeUpdate(Trigger.new, Trigger.oldMap);
29
+ // } else if (Trigger.isDelete) {
30
+ // handler.beforeDelete(Trigger.old, Trigger.oldMap);
31
+ // }
32
+ // } else if (Trigger.isAfter) {
33
+ // if (Trigger.isInsert) {
34
+ // handler.afterInsert(Trigger.new, Trigger.newMap);
35
+ // } else if (Trigger.isUpdate) {
36
+ // handler.afterUpdate(Trigger.new, Trigger.oldMap);
37
+ // } else if (Trigger.isDelete) {
38
+ // handler.afterDelete(Trigger.old, Trigger.oldMap);
39
+ // } else if (Trigger.isUndelete) {
40
+ // handler.afterUndelete(Trigger.new, Trigger.newMap);
41
+ // }
42
+ // }
43
+
44
+ // TODO: Uncomment one option above and remove the other
45
+ }