@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.
- package/README.md +16 -415
- package/package.json +5 -3
- package/skills/building-ui-bundle-app/SKILL.md +325 -0
- package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
- package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
- package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
- package/skills/deploying-ui-bundle/SKILL.md +77 -0
- package/skills/generating-apex/CREDITS.md +30 -0
- package/skills/generating-apex/SKILL.md +399 -0
- package/skills/generating-apex/assets/abstract.cls +132 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/invocable.cls +115 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +199 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +93 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +111 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +108 -0
- package/skills/generating-apex-test/references/async-testing.md +193 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +220 -0
- package/skills/generating-apex-test/references/test-data-factory.md +75 -0
- package/skills/generating-experience-react-site/SKILL.md +20 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +58 -60
- package/skills/generating-ui-bundle-features/SKILL.md +45 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
- package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
- package/skills/searching-media/SKILL.md +342 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
- package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
- package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
- package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
- package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
- package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
- package/skills/building-webapp-data-visualization/SKILL.md +0 -72
- package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
- package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
- package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
- package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
- package/skills/building-webapp-react-components/SKILL.md +0 -96
- package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
- package/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/skills/creating-webapp/SKILL.md +0 -140
- package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
- package/skills/installing-webapp-features/SKILL.md +0 -210
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
- /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
- /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
|
+
}
|