opencode-skills-collection 1.0.186 → 1.0.188
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/bundled-skills/.antigravity-install-manifest.json +5 -1
- package/bundled-skills/3d-web-experience/SKILL.md +152 -37
- package/bundled-skills/agent-evaluation/SKILL.md +1088 -26
- package/bundled-skills/agent-memory-systems/SKILL.md +1037 -25
- package/bundled-skills/agent-tool-builder/SKILL.md +668 -16
- package/bundled-skills/ai-agents-architect/SKILL.md +271 -31
- package/bundled-skills/ai-product/SKILL.md +716 -26
- package/bundled-skills/ai-wrapper-product/SKILL.md +450 -44
- package/bundled-skills/algolia-search/SKILL.md +867 -15
- package/bundled-skills/autonomous-agents/SKILL.md +1033 -26
- package/bundled-skills/aws-serverless/SKILL.md +1046 -35
- package/bundled-skills/azure-functions/SKILL.md +1318 -19
- package/bundled-skills/browser-automation/SKILL.md +1065 -28
- package/bundled-skills/browser-extension-builder/SKILL.md +159 -32
- package/bundled-skills/bullmq-specialist/SKILL.md +347 -16
- package/bundled-skills/clerk-auth/SKILL.md +796 -15
- package/bundled-skills/computer-use-agents/SKILL.md +1870 -28
- package/bundled-skills/context-window-management/SKILL.md +271 -18
- package/bundled-skills/conversation-memory/SKILL.md +453 -24
- package/bundled-skills/crewai/SKILL.md +252 -46
- package/bundled-skills/discord-bot-architect/SKILL.md +1207 -34
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/email-systems/SKILL.md +646 -26
- package/bundled-skills/faf-expert/SKILL.md +221 -0
- package/bundled-skills/faf-wizard/SKILL.md +252 -0
- package/bundled-skills/file-uploads/SKILL.md +212 -11
- package/bundled-skills/firebase/SKILL.md +646 -16
- package/bundled-skills/gcp-cloud-run/SKILL.md +1117 -32
- package/bundled-skills/graphql/SKILL.md +1026 -27
- package/bundled-skills/hubspot-integration/SKILL.md +804 -19
- package/bundled-skills/idea-darwin/SKILL.md +120 -0
- package/bundled-skills/inngest/SKILL.md +431 -16
- package/bundled-skills/interactive-portfolio/SKILL.md +342 -44
- package/bundled-skills/langfuse/SKILL.md +296 -41
- package/bundled-skills/langgraph/SKILL.md +259 -50
- package/bundled-skills/micro-saas-launcher/SKILL.md +343 -44
- package/bundled-skills/neon-postgres/SKILL.md +572 -15
- package/bundled-skills/nextjs-supabase-auth/SKILL.md +269 -21
- package/bundled-skills/notion-template-business/SKILL.md +371 -44
- package/bundled-skills/personal-tool-builder/SKILL.md +537 -44
- package/bundled-skills/plaid-fintech/SKILL.md +825 -19
- package/bundled-skills/prompt-caching/SKILL.md +438 -25
- package/bundled-skills/rag-engineer/SKILL.md +271 -29
- package/bundled-skills/salesforce-development/SKILL.md +912 -19
- package/bundled-skills/satori/SKILL.md +54 -0
- package/bundled-skills/scroll-experience/SKILL.md +381 -44
- package/bundled-skills/segment-cdp/SKILL.md +817 -19
- package/bundled-skills/shopify-apps/SKILL.md +1475 -19
- package/bundled-skills/slack-bot-builder/SKILL.md +1162 -28
- package/bundled-skills/telegram-bot-builder/SKILL.md +152 -37
- package/bundled-skills/telegram-mini-app/SKILL.md +445 -44
- package/bundled-skills/trigger-dev/SKILL.md +916 -27
- package/bundled-skills/twilio-communications/SKILL.md +1310 -28
- package/bundled-skills/upstash-qstash/SKILL.md +898 -27
- package/bundled-skills/vercel-deployment/SKILL.md +637 -39
- package/bundled-skills/viral-generator-builder/SKILL.md +132 -37
- package/bundled-skills/voice-agents/SKILL.md +937 -27
- package/bundled-skills/voice-ai-development/SKILL.md +375 -46
- package/bundled-skills/workflow-automation/SKILL.md +982 -29
- package/bundled-skills/zapier-make-patterns/SKILL.md +772 -27
- package/package.json +1 -1
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: salesforce-development
|
|
3
|
-
description:
|
|
3
|
+
description: Expert patterns for Salesforce platform development including
|
|
4
|
+
Lightning Web Components (LWC), Apex triggers and classes, REST/Bulk APIs,
|
|
5
|
+
Connected Apps, and Salesforce DX with scratch orgs and 2nd generation
|
|
6
|
+
packages (2GP).
|
|
4
7
|
risk: safe
|
|
5
|
-
source:
|
|
6
|
-
date_added:
|
|
8
|
+
source: vibeship-spawner-skills (Apache 2.0)
|
|
9
|
+
date_added: 2026-02-27
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
# Salesforce Development
|
|
10
13
|
|
|
14
|
+
Expert patterns for Salesforce platform development including Lightning Web
|
|
15
|
+
Components (LWC), Apex triggers and classes, REST/Bulk APIs, Connected Apps,
|
|
16
|
+
and Salesforce DX with scratch orgs and 2nd generation packages (2GP).
|
|
17
|
+
|
|
11
18
|
## Patterns
|
|
12
19
|
|
|
13
20
|
### Lightning Web Component with Wire Service
|
|
@@ -16,38 +23,924 @@ Use @wire decorator for reactive data binding with Lightning Data Service
|
|
|
16
23
|
or Apex methods. @wire fits LWC's reactive architecture and enables
|
|
17
24
|
Salesforce performance optimizations.
|
|
18
25
|
|
|
26
|
+
// myComponent.js
|
|
27
|
+
import { LightningElement, wire, api } from 'lwc';
|
|
28
|
+
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
|
|
29
|
+
import getRelatedRecords from '@salesforce/apex/MyController.getRelatedRecords';
|
|
30
|
+
import ACCOUNT_NAME from '@salesforce/schema/Account.Name';
|
|
31
|
+
import ACCOUNT_INDUSTRY from '@salesforce/schema/Account.Industry';
|
|
32
|
+
|
|
33
|
+
const FIELDS = [ACCOUNT_NAME, ACCOUNT_INDUSTRY];
|
|
34
|
+
|
|
35
|
+
export default class MyComponent extends LightningElement {
|
|
36
|
+
@api recordId; // Passed from parent or record page
|
|
37
|
+
|
|
38
|
+
// Wire to Lightning Data Service (preferred for single records)
|
|
39
|
+
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
|
|
40
|
+
account;
|
|
41
|
+
|
|
42
|
+
// Wire to Apex method (for complex queries)
|
|
43
|
+
@wire(getRelatedRecords, { accountId: '$recordId' })
|
|
44
|
+
wiredRecords({ error, data }) {
|
|
45
|
+
if (data) {
|
|
46
|
+
this.relatedRecords = data;
|
|
47
|
+
this.error = undefined;
|
|
48
|
+
} else if (error) {
|
|
49
|
+
this.error = error;
|
|
50
|
+
this.relatedRecords = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get accountName() {
|
|
55
|
+
return getFieldValue(this.account.data, ACCOUNT_NAME);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get isLoading() {
|
|
59
|
+
return !this.account.data && !this.account.error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Reactive: changing recordId automatically re-fetches
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// myComponent.html
|
|
66
|
+
<template>
|
|
67
|
+
<lightning-card title={accountName}>
|
|
68
|
+
<template if:true={isLoading}>
|
|
69
|
+
<lightning-spinner alternative-text="Loading"></lightning-spinner>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<template if:true={account.data}>
|
|
73
|
+
<p>Industry: {industry}</p>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<template if:true={error}>
|
|
77
|
+
<p class="slds-text-color_error">{error.body.message}</p>
|
|
78
|
+
</template>
|
|
79
|
+
</lightning-card>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
// MyController.cls
|
|
83
|
+
public with sharing class MyController {
|
|
84
|
+
@AuraEnabled(cacheable=true)
|
|
85
|
+
public static List<Contact> getRelatedRecords(Id accountId) {
|
|
86
|
+
return [
|
|
87
|
+
SELECT Id, Name, Email, Phone
|
|
88
|
+
FROM Contact
|
|
89
|
+
WHERE AccountId = :accountId
|
|
90
|
+
WITH SECURITY_ENFORCED
|
|
91
|
+
LIMIT 100
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
### Context
|
|
97
|
+
|
|
98
|
+
- building LWC components
|
|
99
|
+
- fetching Salesforce data
|
|
100
|
+
- reactive UI
|
|
101
|
+
|
|
19
102
|
### Bulkified Apex Trigger with Handler Pattern
|
|
20
103
|
|
|
21
104
|
Apex triggers must be bulkified to handle 200+ records per transaction.
|
|
22
105
|
Use handler pattern for separation of concerns, testability, and
|
|
23
106
|
recursion prevention.
|
|
24
107
|
|
|
108
|
+
// AccountTrigger.trigger
|
|
109
|
+
trigger AccountTrigger on Account (
|
|
110
|
+
before insert, before update, before delete,
|
|
111
|
+
after insert, after update, after delete, after undelete
|
|
112
|
+
) {
|
|
113
|
+
new AccountTriggerHandler().run();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// TriggerHandler.cls (base class)
|
|
117
|
+
public virtual class TriggerHandler {
|
|
118
|
+
// Recursion prevention
|
|
119
|
+
private static Set<String> executedHandlers = new Set<String>();
|
|
120
|
+
|
|
121
|
+
public void run() {
|
|
122
|
+
String handlerName = String.valueOf(this).split(':')[0];
|
|
123
|
+
|
|
124
|
+
// Prevent recursion
|
|
125
|
+
String contextKey = handlerName + '_' + Trigger.operationType;
|
|
126
|
+
if (executedHandlers.contains(contextKey)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
executedHandlers.add(contextKey);
|
|
130
|
+
|
|
131
|
+
switch on Trigger.operationType {
|
|
132
|
+
when BEFORE_INSERT { this.beforeInsert(); }
|
|
133
|
+
when BEFORE_UPDATE { this.beforeUpdate(); }
|
|
134
|
+
when BEFORE_DELETE { this.beforeDelete(); }
|
|
135
|
+
when AFTER_INSERT { this.afterInsert(); }
|
|
136
|
+
when AFTER_UPDATE { this.afterUpdate(); }
|
|
137
|
+
when AFTER_DELETE { this.afterDelete(); }
|
|
138
|
+
when AFTER_UNDELETE { this.afterUndelete(); }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Override in child classes
|
|
143
|
+
protected virtual void beforeInsert() {}
|
|
144
|
+
protected virtual void beforeUpdate() {}
|
|
145
|
+
protected virtual void beforeDelete() {}
|
|
146
|
+
protected virtual void afterInsert() {}
|
|
147
|
+
protected virtual void afterUpdate() {}
|
|
148
|
+
protected virtual void afterDelete() {}
|
|
149
|
+
protected virtual void afterUndelete() {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// AccountTriggerHandler.cls
|
|
153
|
+
public class AccountTriggerHandler extends TriggerHandler {
|
|
154
|
+
private List<Account> newAccounts;
|
|
155
|
+
private List<Account> oldAccounts;
|
|
156
|
+
private Map<Id, Account> newMap;
|
|
157
|
+
private Map<Id, Account> oldMap;
|
|
158
|
+
|
|
159
|
+
public AccountTriggerHandler() {
|
|
160
|
+
this.newAccounts = (List<Account>) Trigger.new;
|
|
161
|
+
this.oldAccounts = (List<Account>) Trigger.old;
|
|
162
|
+
this.newMap = (Map<Id, Account>) Trigger.newMap;
|
|
163
|
+
this.oldMap = (Map<Id, Account>) Trigger.oldMap;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
protected override void afterInsert() {
|
|
167
|
+
createDefaultContacts();
|
|
168
|
+
notifySlack();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected override void afterUpdate() {
|
|
172
|
+
handleIndustryChange();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// BULKIFIED: Query once, update once
|
|
176
|
+
private void createDefaultContacts() {
|
|
177
|
+
List<Contact> contactsToInsert = new List<Contact>();
|
|
178
|
+
|
|
179
|
+
for (Account acc : newAccounts) {
|
|
180
|
+
if (acc.Type == 'Prospect') {
|
|
181
|
+
contactsToInsert.add(new Contact(
|
|
182
|
+
AccountId = acc.Id,
|
|
183
|
+
LastName = 'Primary Contact',
|
|
184
|
+
Email = 'contact@' + acc.Website
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!contactsToInsert.isEmpty()) {
|
|
190
|
+
insert contactsToInsert; // Single DML for all
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private void handleIndustryChange() {
|
|
195
|
+
Set<Id> changedAccountIds = new Set<Id>();
|
|
196
|
+
|
|
197
|
+
for (Account acc : newAccounts) {
|
|
198
|
+
Account oldAcc = oldMap.get(acc.Id);
|
|
199
|
+
if (acc.Industry != oldAcc.Industry) {
|
|
200
|
+
changedAccountIds.add(acc.Id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!changedAccountIds.isEmpty()) {
|
|
205
|
+
// Queue async processing for heavy work
|
|
206
|
+
System.enqueueJob(new IndustryChangeQueueable(changedAccountIds));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private void notifySlack() {
|
|
211
|
+
// Offload callouts to async
|
|
212
|
+
List<Id> accountIds = new List<Id>(newMap.keySet());
|
|
213
|
+
System.enqueueJob(new SlackNotificationQueueable(accountIds));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
### Context
|
|
218
|
+
|
|
219
|
+
- apex triggers
|
|
220
|
+
- data operations
|
|
221
|
+
- automation
|
|
222
|
+
|
|
25
223
|
### Queueable Apex for Async Processing
|
|
26
224
|
|
|
27
225
|
Use Queueable Apex for async processing with support for non-primitive
|
|
28
226
|
types, monitoring via AsyncApexJob, and job chaining. Limit: 50 jobs
|
|
29
227
|
per transaction, 1 child job when chaining.
|
|
30
228
|
|
|
31
|
-
|
|
229
|
+
// IndustryChangeQueueable.cls
|
|
230
|
+
public class IndustryChangeQueueable implements Queueable, Database.AllowsCallouts {
|
|
231
|
+
private Set<Id> accountIds;
|
|
232
|
+
private Integer retryCount;
|
|
233
|
+
|
|
234
|
+
public IndustryChangeQueueable(Set<Id> accountIds) {
|
|
235
|
+
this(accountIds, 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public IndustryChangeQueueable(Set<Id> accountIds, Integer retryCount) {
|
|
239
|
+
this.accountIds = accountIds;
|
|
240
|
+
this.retryCount = retryCount;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public void execute(QueueableContext context) {
|
|
244
|
+
try {
|
|
245
|
+
// Query with fresh data
|
|
246
|
+
List<Account> accounts = [
|
|
247
|
+
SELECT Id, Name, Industry, OwnerId
|
|
248
|
+
FROM Account
|
|
249
|
+
WHERE Id IN :accountIds
|
|
250
|
+
WITH SECURITY_ENFORCED
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
// Process and make callout
|
|
254
|
+
for (Account acc : accounts) {
|
|
255
|
+
syncToExternalSystem(acc);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Update records
|
|
259
|
+
updateRelatedOpportunities(accountIds);
|
|
260
|
+
|
|
261
|
+
} catch (Exception e) {
|
|
262
|
+
handleError(e);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private void syncToExternalSystem(Account acc) {
|
|
267
|
+
HttpRequest req = new HttpRequest();
|
|
268
|
+
req.setEndpoint('callout:ExternalCRM/accounts');
|
|
269
|
+
req.setMethod('POST');
|
|
270
|
+
req.setHeader('Content-Type', 'application/json');
|
|
271
|
+
req.setBody(JSON.serialize(new Map<String, Object>{
|
|
272
|
+
'salesforceId' => acc.Id,
|
|
273
|
+
'name' => acc.Name,
|
|
274
|
+
'industry' => acc.Industry
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
Http http = new Http();
|
|
278
|
+
HttpResponse res = http.send(req);
|
|
279
|
+
|
|
280
|
+
if (res.getStatusCode() != 200 && res.getStatusCode() != 201) {
|
|
281
|
+
throw new CalloutException('Sync failed: ' + res.getBody());
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private void updateRelatedOpportunities(Set<Id> accIds) {
|
|
286
|
+
List<Opportunity> oppsToUpdate = [
|
|
287
|
+
SELECT Id, Industry__c, AccountId
|
|
288
|
+
FROM Opportunity
|
|
289
|
+
WHERE AccountId IN :accIds
|
|
290
|
+
WITH SECURITY_ENFORCED
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
Map<Id, Account> accountMap = new Map<Id, Account>([
|
|
294
|
+
SELECT Id, Industry FROM Account WHERE Id IN :accIds
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
for (Opportunity opp : oppsToUpdate) {
|
|
298
|
+
opp.Industry__c = accountMap.get(opp.AccountId).Industry;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!oppsToUpdate.isEmpty()) {
|
|
302
|
+
update oppsToUpdate;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private void handleError(Exception e) {
|
|
307
|
+
// Log error
|
|
308
|
+
System.debug(LoggingLevel.ERROR, 'Queueable failed: ' + e.getMessage());
|
|
309
|
+
|
|
310
|
+
// Retry with exponential backoff (max 3 retries)
|
|
311
|
+
if (retryCount < 3) {
|
|
312
|
+
// Chain new job for retry
|
|
313
|
+
System.enqueueJob(new IndustryChangeQueueable(accountIds, retryCount + 1));
|
|
314
|
+
} else {
|
|
315
|
+
// Create error record for monitoring
|
|
316
|
+
insert new Integration_Error__c(
|
|
317
|
+
Type__c = 'Industry Sync',
|
|
318
|
+
Message__c = e.getMessage(),
|
|
319
|
+
Stack_Trace__c = e.getStackTraceString(),
|
|
320
|
+
Record_Ids__c = String.join(new List<Id>(accountIds), ',')
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
### Context
|
|
327
|
+
|
|
328
|
+
- async processing
|
|
329
|
+
- long-running operations
|
|
330
|
+
- callouts from triggers
|
|
331
|
+
|
|
332
|
+
### REST API Integration with Connected App
|
|
333
|
+
|
|
334
|
+
External integrations use Connected Apps with OAuth 2.0. JWT Bearer flow
|
|
335
|
+
for server-to-server, Web Server flow for user-facing apps. Always use
|
|
336
|
+
Named Credentials for secure callout configuration.
|
|
337
|
+
|
|
338
|
+
// Node.js - JWT Bearer Flow (server-to-server)
|
|
339
|
+
import jwt from 'jsonwebtoken';
|
|
340
|
+
import fs from 'fs';
|
|
341
|
+
|
|
342
|
+
class SalesforceClient {
|
|
343
|
+
private accessToken: string | null = null;
|
|
344
|
+
private instanceUrl: string | null = null;
|
|
345
|
+
private tokenExpiry: number = 0;
|
|
346
|
+
|
|
347
|
+
constructor(
|
|
348
|
+
private clientId: string,
|
|
349
|
+
private username: string,
|
|
350
|
+
private privateKeyPath: string,
|
|
351
|
+
private loginUrl: string = 'https://login.salesforce.com'
|
|
352
|
+
) {}
|
|
353
|
+
|
|
354
|
+
async authenticate(): Promise<void> {
|
|
355
|
+
// Check if token is still valid (5 min buffer)
|
|
356
|
+
if (this.accessToken && Date.now() < this.tokenExpiry - 300000) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const privateKey = fs.readFileSync(this.privateKeyPath, 'utf8');
|
|
361
|
+
|
|
362
|
+
// Create JWT assertion
|
|
363
|
+
const claim = {
|
|
364
|
+
iss: this.clientId,
|
|
365
|
+
sub: this.username,
|
|
366
|
+
aud: this.loginUrl,
|
|
367
|
+
exp: Math.floor(Date.now() / 1000) + 300 // 5 minutes
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const assertion = jwt.sign(claim, privateKey, { algorithm: 'RS256' });
|
|
371
|
+
|
|
372
|
+
// Exchange JWT for access token
|
|
373
|
+
const response = await fetch(`${this.loginUrl}/services/oauth2/token`, {
|
|
374
|
+
method: 'POST',
|
|
375
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
376
|
+
body: new URLSearchParams({
|
|
377
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
378
|
+
assertion
|
|
379
|
+
})
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
const error = await response.json();
|
|
384
|
+
throw new Error(`Auth failed: ${error.error_description}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const data = await response.json();
|
|
388
|
+
this.accessToken = data.access_token;
|
|
389
|
+
this.instanceUrl = data.instance_url;
|
|
390
|
+
this.tokenExpiry = Date.now() + 7200000; // 2 hours
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async query(soql: string): Promise<any> {
|
|
394
|
+
await this.authenticate();
|
|
395
|
+
|
|
396
|
+
const response = await fetch(
|
|
397
|
+
`${this.instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(soql)}`,
|
|
398
|
+
{
|
|
399
|
+
headers: {
|
|
400
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
401
|
+
'Content-Type': 'application/json'
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
await this.handleError(response);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return response.json();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async createRecord(sobject: string, data: object): Promise<any> {
|
|
414
|
+
await this.authenticate();
|
|
415
|
+
|
|
416
|
+
const response = await fetch(
|
|
417
|
+
`${this.instanceUrl}/services/data/v59.0/sobjects/${sobject}`,
|
|
418
|
+
{
|
|
419
|
+
method: 'POST',
|
|
420
|
+
headers: {
|
|
421
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
422
|
+
'Content-Type': 'application/json'
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify(data)
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (!response.ok) {
|
|
429
|
+
await this.handleError(response);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return response.json();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private async handleError(response: Response): Promise<never> {
|
|
436
|
+
const error = await response.json();
|
|
437
|
+
|
|
438
|
+
if (response.status === 401) {
|
|
439
|
+
// Token expired, clear and retry
|
|
440
|
+
this.accessToken = null;
|
|
441
|
+
throw new Error('Session expired, retry required');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw new Error(`API Error: ${JSON.stringify(error)}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Usage
|
|
449
|
+
const sf = new SalesforceClient(
|
|
450
|
+
process.env.SF_CLIENT_ID!,
|
|
451
|
+
process.env.SF_USERNAME!,
|
|
452
|
+
'./certificates/server.key'
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const accounts = await sf.query(
|
|
456
|
+
"SELECT Id, Name FROM Account WHERE CreatedDate = TODAY"
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
### Context
|
|
460
|
+
|
|
461
|
+
- external integration
|
|
462
|
+
- REST API access
|
|
463
|
+
- connected apps
|
|
464
|
+
|
|
465
|
+
### Bulk API 2.0 for Large Data Operations
|
|
466
|
+
|
|
467
|
+
Use Bulk API 2.0 for operations on 10K+ records. Asynchronous processing
|
|
468
|
+
with job-based workflow. Part of REST API with streamlined interface
|
|
469
|
+
compared to original Bulk API.
|
|
470
|
+
|
|
471
|
+
// Node.js - Bulk API 2.0 insert
|
|
472
|
+
class SalesforceBulkClient extends SalesforceClient {
|
|
473
|
+
|
|
474
|
+
async bulkInsert(sobject: string, records: object[]): Promise<any> {
|
|
475
|
+
await this.authenticate();
|
|
476
|
+
|
|
477
|
+
// Step 1: Create job
|
|
478
|
+
const job = await this.createBulkJob(sobject, 'insert');
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// Step 2: Upload data (CSV format)
|
|
482
|
+
await this.uploadJobData(job.id, records);
|
|
483
|
+
|
|
484
|
+
// Step 3: Close job to start processing
|
|
485
|
+
await this.closeJob(job.id);
|
|
486
|
+
|
|
487
|
+
// Step 4: Poll for completion
|
|
488
|
+
return await this.waitForJobCompletion(job.id);
|
|
489
|
+
|
|
490
|
+
} catch (error) {
|
|
491
|
+
// Abort job on error
|
|
492
|
+
await this.abortJob(job.id);
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async createBulkJob(sobject: string, operation: string): Promise<any> {
|
|
498
|
+
const response = await fetch(
|
|
499
|
+
`${this.instanceUrl}/services/data/v59.0/jobs/ingest`,
|
|
500
|
+
{
|
|
501
|
+
method: 'POST',
|
|
502
|
+
headers: {
|
|
503
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
504
|
+
'Content-Type': 'application/json'
|
|
505
|
+
},
|
|
506
|
+
body: JSON.stringify({
|
|
507
|
+
object: sobject,
|
|
508
|
+
operation,
|
|
509
|
+
contentType: 'CSV',
|
|
510
|
+
lineEnding: 'LF'
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
return response.json();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private async uploadJobData(jobId: string, records: object[]): Promise<void> {
|
|
519
|
+
// Convert to CSV
|
|
520
|
+
const csv = this.recordsToCSV(records);
|
|
521
|
+
|
|
522
|
+
await fetch(
|
|
523
|
+
`${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}/batches`,
|
|
524
|
+
{
|
|
525
|
+
method: 'PUT',
|
|
526
|
+
headers: {
|
|
527
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
528
|
+
'Content-Type': 'text/csv'
|
|
529
|
+
},
|
|
530
|
+
body: csv
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private async closeJob(jobId: string): Promise<void> {
|
|
536
|
+
await fetch(
|
|
537
|
+
`${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}`,
|
|
538
|
+
{
|
|
539
|
+
method: 'PATCH',
|
|
540
|
+
headers: {
|
|
541
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
542
|
+
'Content-Type': 'application/json'
|
|
543
|
+
},
|
|
544
|
+
body: JSON.stringify({ state: 'UploadComplete' })
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private async waitForJobCompletion(jobId: string): Promise<any> {
|
|
550
|
+
const maxWaitTime = 10 * 60 * 1000; // 10 minutes
|
|
551
|
+
const pollInterval = 5000; // 5 seconds
|
|
552
|
+
const startTime = Date.now();
|
|
553
|
+
|
|
554
|
+
while (Date.now() - startTime < maxWaitTime) {
|
|
555
|
+
const response = await fetch(
|
|
556
|
+
`${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}`,
|
|
557
|
+
{
|
|
558
|
+
headers: { 'Authorization': `Bearer ${this.accessToken}` }
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const job = await response.json();
|
|
563
|
+
|
|
564
|
+
if (job.state === 'JobComplete') {
|
|
565
|
+
// Get results
|
|
566
|
+
return {
|
|
567
|
+
success: job.numberRecordsProcessed - job.numberRecordsFailed,
|
|
568
|
+
failed: job.numberRecordsFailed,
|
|
569
|
+
failedResults: job.numberRecordsFailed > 0
|
|
570
|
+
? await this.getFailedResults(jobId)
|
|
571
|
+
: []
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (job.state === 'Failed' || job.state === 'Aborted') {
|
|
576
|
+
throw new Error(`Bulk job failed: ${job.state}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
throw new Error('Bulk job timeout');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private async getFailedResults(jobId: string): Promise<any[]> {
|
|
586
|
+
const response = await fetch(
|
|
587
|
+
`${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}/failedResults`,
|
|
588
|
+
{
|
|
589
|
+
headers: { 'Authorization': `Bearer ${this.accessToken}` }
|
|
590
|
+
}
|
|
591
|
+
);
|
|
32
592
|
|
|
33
|
-
|
|
593
|
+
const csv = await response.text();
|
|
594
|
+
return this.parseCSV(csv);
|
|
595
|
+
}
|
|
34
596
|
|
|
35
|
-
|
|
597
|
+
private recordsToCSV(records: object[]): string {
|
|
598
|
+
if (records.length === 0) return '';
|
|
36
599
|
|
|
37
|
-
|
|
600
|
+
const headers = Object.keys(records[0]);
|
|
601
|
+
const rows = records.map(r =>
|
|
602
|
+
headers.map(h => this.escapeCSV(r[h])).join(',')
|
|
603
|
+
);
|
|
38
604
|
|
|
39
|
-
|
|
605
|
+
return [headers.join(','), ...rows].join('\n');
|
|
606
|
+
}
|
|
40
607
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
608
|
+
private escapeCSV(value: any): string {
|
|
609
|
+
if (value === null || value === undefined) return '';
|
|
610
|
+
const str = String(value);
|
|
611
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
612
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
613
|
+
}
|
|
614
|
+
return str;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
### Context
|
|
619
|
+
|
|
620
|
+
- large data volumes
|
|
621
|
+
- data migration
|
|
622
|
+
- bulk operations
|
|
623
|
+
|
|
624
|
+
### Salesforce DX with Scratch Orgs
|
|
625
|
+
|
|
626
|
+
Source-driven development with disposable scratch orgs for isolated
|
|
627
|
+
testing. Scratch orgs exist 7-30 days and can be created throughout
|
|
628
|
+
the day, unlike sandbox refresh limits.
|
|
629
|
+
|
|
630
|
+
// project-scratch-def.json - Scratch org definition
|
|
631
|
+
{
|
|
632
|
+
"orgName": "MyApp Dev Org",
|
|
633
|
+
"edition": "Developer",
|
|
634
|
+
"features": ["EnableSetPasswordInApi", "Communities"],
|
|
635
|
+
"settings": {
|
|
636
|
+
"lightningExperienceSettings": {
|
|
637
|
+
"enableS1DesktopEnabled": true
|
|
638
|
+
},
|
|
639
|
+
"mobileSettings": {
|
|
640
|
+
"enableS1EncryptedStoragePref2": false
|
|
641
|
+
},
|
|
642
|
+
"securitySettings": {
|
|
643
|
+
"passwordPolicies": {
|
|
644
|
+
"enableSetPasswordInApi": true
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// sfdx-project.json - Project configuration
|
|
651
|
+
{
|
|
652
|
+
"packageDirectories": [
|
|
653
|
+
{
|
|
654
|
+
"path": "force-app",
|
|
655
|
+
"default": true,
|
|
656
|
+
"package": "MyPackage",
|
|
657
|
+
"versionName": "ver 1.0",
|
|
658
|
+
"versionNumber": "1.0.0.NEXT",
|
|
659
|
+
"dependencies": [
|
|
660
|
+
{
|
|
661
|
+
"package": "SomePackage@2.0.0"
|
|
662
|
+
}
|
|
663
|
+
]
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
"namespace": "myns",
|
|
667
|
+
"sfdcLoginUrl": "https://login.salesforce.com",
|
|
668
|
+
"sourceApiVersion": "59.0"
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# Development workflow commands
|
|
672
|
+
# 1. Create scratch org
|
|
673
|
+
sf org create scratch \
|
|
674
|
+
--definition-file config/project-scratch-def.json \
|
|
675
|
+
--alias myapp-dev \
|
|
676
|
+
--duration-days 7 \
|
|
677
|
+
--set-default
|
|
678
|
+
|
|
679
|
+
# 2. Push source to scratch org
|
|
680
|
+
sf project deploy start --target-org myapp-dev
|
|
681
|
+
|
|
682
|
+
# 3. Assign permission set
|
|
683
|
+
sf org assign permset --name MyApp_Admin --target-org myapp-dev
|
|
684
|
+
|
|
685
|
+
# 4. Import sample data
|
|
686
|
+
sf data import tree --plan data/sample-data-plan.json --target-org myapp-dev
|
|
687
|
+
|
|
688
|
+
# 5. Open org
|
|
689
|
+
sf org open --target-org myapp-dev
|
|
690
|
+
|
|
691
|
+
# 6. Run tests
|
|
692
|
+
sf apex run test \
|
|
693
|
+
--code-coverage \
|
|
694
|
+
--result-format human \
|
|
695
|
+
--wait 10 \
|
|
696
|
+
--target-org myapp-dev
|
|
697
|
+
|
|
698
|
+
# 7. Pull changes back
|
|
699
|
+
sf project retrieve start --target-org myapp-dev
|
|
700
|
+
|
|
701
|
+
### Context
|
|
702
|
+
|
|
703
|
+
- development workflow
|
|
704
|
+
- CI/CD
|
|
705
|
+
- testing
|
|
706
|
+
|
|
707
|
+
### 2nd Generation Package (2GP) Development
|
|
708
|
+
|
|
709
|
+
2GP replaces 1GP with source-driven, modular packaging. Requires Dev Hub
|
|
710
|
+
with 2GP enabled, namespace linked, and 75% code coverage for promoted
|
|
711
|
+
packages.
|
|
712
|
+
|
|
713
|
+
# Enable Dev Hub and 2GP in Setup:
|
|
714
|
+
# Setup > Dev Hub > Enable Dev Hub
|
|
715
|
+
# Setup > Dev Hub > Enable Unlocked Packages and 2GP
|
|
716
|
+
|
|
717
|
+
# Link namespace (required for managed packages)
|
|
718
|
+
sf package create \
|
|
719
|
+
--name "MyManagedPackage" \
|
|
720
|
+
--package-type Managed \
|
|
721
|
+
--path force-app \
|
|
722
|
+
--target-dev-hub DevHub
|
|
723
|
+
|
|
724
|
+
# Create package version (beta)
|
|
725
|
+
sf package version create \
|
|
726
|
+
--package "MyManagedPackage" \
|
|
727
|
+
--installation-key-bypass \
|
|
728
|
+
--wait 30 \
|
|
729
|
+
--code-coverage \
|
|
730
|
+
--target-dev-hub DevHub
|
|
731
|
+
|
|
732
|
+
# Check version status
|
|
733
|
+
sf package version list --packages "MyManagedPackage" --target-dev-hub DevHub
|
|
734
|
+
|
|
735
|
+
# Promote to released (requires 75% coverage)
|
|
736
|
+
sf package version promote \
|
|
737
|
+
--package "MyManagedPackage@1.0.0-1" \
|
|
738
|
+
--target-dev-hub DevHub
|
|
739
|
+
|
|
740
|
+
# Install in sandbox for testing
|
|
741
|
+
sf package install \
|
|
742
|
+
--package "MyManagedPackage@1.0.0-1" \
|
|
743
|
+
--target-org MySandbox \
|
|
744
|
+
--wait 20
|
|
745
|
+
|
|
746
|
+
# CI/CD Pipeline (GitHub Actions)
|
|
747
|
+
# .github/workflows/salesforce-ci.yml
|
|
748
|
+
name: Salesforce CI
|
|
749
|
+
|
|
750
|
+
on:
|
|
751
|
+
push:
|
|
752
|
+
branches: [main, develop]
|
|
753
|
+
pull_request:
|
|
754
|
+
branches: [main]
|
|
755
|
+
|
|
756
|
+
jobs:
|
|
757
|
+
validate:
|
|
758
|
+
runs-on: ubuntu-latest
|
|
759
|
+
steps:
|
|
760
|
+
- uses: actions/checkout@v4
|
|
761
|
+
|
|
762
|
+
- name: Install Salesforce CLI
|
|
763
|
+
run: npm install -g @salesforce/cli
|
|
764
|
+
|
|
765
|
+
- name: Authenticate Dev Hub
|
|
766
|
+
run: |
|
|
767
|
+
echo "${{ secrets.SFDX_AUTH_URL }}" > auth.txt
|
|
768
|
+
sf org login sfdx-url --sfdx-url-file auth.txt --alias DevHub --set-default-dev-hub
|
|
769
|
+
|
|
770
|
+
- name: Create Scratch Org
|
|
771
|
+
run: |
|
|
772
|
+
sf org create scratch \
|
|
773
|
+
--definition-file config/project-scratch-def.json \
|
|
774
|
+
--alias ci-scratch \
|
|
775
|
+
--duration-days 1 \
|
|
776
|
+
--set-default
|
|
777
|
+
|
|
778
|
+
- name: Deploy Source
|
|
779
|
+
run: sf project deploy start --target-org ci-scratch
|
|
780
|
+
|
|
781
|
+
- name: Run Tests
|
|
782
|
+
run: |
|
|
783
|
+
sf apex run test \
|
|
784
|
+
--code-coverage \
|
|
785
|
+
--result-format human \
|
|
786
|
+
--wait 20 \
|
|
787
|
+
--target-org ci-scratch
|
|
788
|
+
|
|
789
|
+
- name: Delete Scratch Org
|
|
790
|
+
if: always()
|
|
791
|
+
run: sf org delete scratch --target-org ci-scratch --no-prompt
|
|
792
|
+
|
|
793
|
+
### Context
|
|
794
|
+
|
|
795
|
+
- packaging
|
|
796
|
+
- ISV development
|
|
797
|
+
- AppExchange
|
|
798
|
+
|
|
799
|
+
## Sharp Edges
|
|
800
|
+
|
|
801
|
+
### Governor Limits Apply Per Transaction, Not Per Record
|
|
802
|
+
|
|
803
|
+
Severity: CRITICAL
|
|
804
|
+
|
|
805
|
+
### @wire Results Are Cached and May Be Stale
|
|
806
|
+
|
|
807
|
+
Severity: HIGH
|
|
808
|
+
|
|
809
|
+
### LWC Properties Are Case-Sensitive
|
|
810
|
+
|
|
811
|
+
Severity: MEDIUM
|
|
812
|
+
|
|
813
|
+
### Null Pointer Exceptions in Apex Collections
|
|
814
|
+
|
|
815
|
+
Severity: HIGH
|
|
816
|
+
|
|
817
|
+
### Trigger Recursion Causes Infinite Loops
|
|
818
|
+
|
|
819
|
+
Severity: CRITICAL
|
|
820
|
+
|
|
821
|
+
### Cannot Make Callouts from Synchronous Triggers
|
|
822
|
+
|
|
823
|
+
Severity: HIGH
|
|
824
|
+
|
|
825
|
+
### Cannot Mix Setup and Non-Setup DML
|
|
826
|
+
|
|
827
|
+
Severity: HIGH
|
|
828
|
+
|
|
829
|
+
### Dynamic SOQL Is Vulnerable to Injection
|
|
830
|
+
|
|
831
|
+
Severity: CRITICAL
|
|
832
|
+
|
|
833
|
+
### Scratch Orgs Expire and Lose All Data
|
|
834
|
+
|
|
835
|
+
Severity: MEDIUM
|
|
836
|
+
|
|
837
|
+
### API Version Mismatches Cause Silent Failures
|
|
838
|
+
|
|
839
|
+
Severity: MEDIUM
|
|
840
|
+
|
|
841
|
+
## Validation Checks
|
|
842
|
+
|
|
843
|
+
### SOQL Query Inside Loop
|
|
844
|
+
|
|
845
|
+
Severity: ERROR
|
|
846
|
+
|
|
847
|
+
SOQL in loops causes governor limit exceptions with bulk data
|
|
848
|
+
|
|
849
|
+
Message: SOQL query inside loop. Query once outside the loop and use a Map.
|
|
850
|
+
|
|
851
|
+
### DML Operation Inside Loop
|
|
852
|
+
|
|
853
|
+
Severity: ERROR
|
|
854
|
+
|
|
855
|
+
DML in loops hits 150 statement limit
|
|
856
|
+
|
|
857
|
+
Message: DML operation inside loop. Collect records and perform single DML outside loop.
|
|
858
|
+
|
|
859
|
+
### HTTP Callout in Trigger
|
|
860
|
+
|
|
861
|
+
Severity: ERROR
|
|
862
|
+
|
|
863
|
+
Synchronous triggers cannot make callouts
|
|
864
|
+
|
|
865
|
+
Message: Callout in trigger. Use @future(callout=true) or Queueable with Database.AllowsCallouts.
|
|
866
|
+
|
|
867
|
+
### Potential SOQL Injection
|
|
868
|
+
|
|
869
|
+
Severity: ERROR
|
|
870
|
+
|
|
871
|
+
Dynamic SOQL with string concatenation is vulnerable
|
|
872
|
+
|
|
873
|
+
Message: Dynamic SOQL with concatenation. Use bind variables or String.escapeSingleQuotes().
|
|
874
|
+
|
|
875
|
+
### Missing WITH SECURITY_ENFORCED
|
|
876
|
+
|
|
877
|
+
Severity: WARNING
|
|
878
|
+
|
|
879
|
+
SOQL should enforce FLS/CRUD permissions
|
|
880
|
+
|
|
881
|
+
Message: SOQL without security enforcement. Add WITH SECURITY_ENFORCED.
|
|
882
|
+
|
|
883
|
+
### Hardcoded Salesforce ID
|
|
884
|
+
|
|
885
|
+
Severity: WARNING
|
|
886
|
+
|
|
887
|
+
Record IDs differ between orgs
|
|
888
|
+
|
|
889
|
+
Message: Hardcoded Salesforce ID. Query by DeveloperName or ExternalId instead.
|
|
890
|
+
|
|
891
|
+
### Hardcoded Credentials
|
|
892
|
+
|
|
893
|
+
Severity: ERROR
|
|
894
|
+
|
|
895
|
+
Credentials must use Named Credentials or Custom Metadata
|
|
896
|
+
|
|
897
|
+
Message: Hardcoded credentials. Use Named Credentials or Custom Metadata.
|
|
898
|
+
|
|
899
|
+
### Direct DOM Manipulation in LWC
|
|
900
|
+
|
|
901
|
+
Severity: WARNING
|
|
902
|
+
|
|
903
|
+
LWC uses shadow DOM, direct manipulation breaks encapsulation
|
|
904
|
+
|
|
905
|
+
Message: Direct DOM access in LWC. Use this.template.querySelector() or data binding.
|
|
906
|
+
|
|
907
|
+
### Reactive Property Without @track
|
|
908
|
+
|
|
909
|
+
Severity: INFO
|
|
910
|
+
|
|
911
|
+
Complex object properties need @track for reactivity
|
|
912
|
+
|
|
913
|
+
Message: Object assignment may need @track for reactivity (post-Spring '20 objects are auto-tracked).
|
|
914
|
+
|
|
915
|
+
### Wire Without Refresh After DML
|
|
916
|
+
|
|
917
|
+
Severity: WARNING
|
|
918
|
+
|
|
919
|
+
Cached wire data becomes stale after updates
|
|
920
|
+
|
|
921
|
+
Message: DML after @wire without refreshApex. Data may be stale.
|
|
922
|
+
|
|
923
|
+
## Collaboration
|
|
924
|
+
|
|
925
|
+
### Delegation Triggers
|
|
926
|
+
|
|
927
|
+
- user needs external API integration -> backend (REST API design, external system sync)
|
|
928
|
+
- user needs complex UI beyond LWC -> frontend (Custom portal with React/Next.js)
|
|
929
|
+
- user needs HubSpot integration -> hubspot-integration (Salesforce-HubSpot sync patterns)
|
|
930
|
+
- user needs data warehouse sync -> data-engineer (ETL from Salesforce to warehouse)
|
|
931
|
+
- user needs payment processing -> stripe-integration (Beyond Salesforce Billing)
|
|
932
|
+
- user needs advanced auth -> auth-specialist (SSO, SAML, custom portals)
|
|
51
933
|
|
|
52
934
|
## When to Use
|
|
53
|
-
|
|
935
|
+
|
|
936
|
+
- User mentions or implies: salesforce
|
|
937
|
+
- User mentions or implies: sfdc
|
|
938
|
+
- User mentions or implies: apex
|
|
939
|
+
- User mentions or implies: lwc
|
|
940
|
+
- User mentions or implies: lightning web components
|
|
941
|
+
- User mentions or implies: sfdx
|
|
942
|
+
- User mentions or implies: scratch org
|
|
943
|
+
- User mentions or implies: visualforce
|
|
944
|
+
- User mentions or implies: soql
|
|
945
|
+
- User mentions or implies: governor limits
|
|
946
|
+
- User mentions or implies: connected app
|