소스 정보
- 저장소
- BEKO2210/Firstbrain
- 최근 소스 활동
- 2026년 5월 17일 12:40
- 감지된 SKILL.md 언어
- 영어
- 스타
- 15
- 포크
- 2
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/BEKO2210/Firstbrain --skill salesforce-development명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Arquitecto de Soluciones Principal y Consultor Tecnológico de Andru.ia. Diagnostica y traza la hoja de ruta óptima para proyectos de IA en español.
Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.
Ingeniero de Sistemas de Andru.ia. Diseña, redacta y despliega nuevas habilidades (skills) dentro del repositorio siguiendo el Estándar de Diamante.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | salesforce-development |
| description | Expert patterns for Salesforce platform development including |
| type | skill |
| created | 2026-02-27T00:00:00.000Z |
| domain | business-marketing |
| category | sales-crm |
| risk | safe |
| source | vibeship-spawner-skills (Apache 2.0) |
| tags | ["skill","business-marketing","sales-crm","salesforce"] |
Expert patterns for Salesforce platform development including Lightning Web Components (LWC), Apex triggers and classes, REST/Bulk APIs, Connected Apps, and Salesforce DX with scratch orgs and 2nd generation packages (2GP).
Use @wire decorator for reactive data binding with Lightning Data Service or Apex methods. @wire fits LWC's reactive architecture and enables Salesforce performance optimizations.
// myComponent.js import { LightningElement, wire, api } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import getRelatedRecords from '@salesforce/apex/MyController.getRelatedRecords'; import ACCOUNT_NAME from '@salesforce/schema/Account.Name'; import ACCOUNT_INDUSTRY from '@salesforce/schema/Account.Industry';
const FIELDS = [ACCOUNT_NAME, ACCOUNT_INDUSTRY];
export default class MyComponent extends LightningElement { @api recordId; // Passed from parent or record page
// Wire to Lightning Data Service (preferred for single records) @wire(getRecord, { recordId: '$recordId', fields: FIELDS }) account;
// Wire to Apex method (for complex queries) @wire(getRelatedRecords, { accountId: '$recordId' }) wiredRecords({ error, data }) { if (data) { this.relatedRecords = data; this.error = undefined; } else if (error) { this.error = error; this.relatedRecords = undefined; } }
get accountName() { return getFieldValue(this.account.data, ACCOUNT_NAME); }
get isLoading() { return !this.account.data && !this.account.error; }
// Reactive: changing recordId automatically re-fetches }
// myComponent.html
// MyController.cls public with sharing class MyController { @AuraEnabled(cacheable=true) public static List getRelatedRecords(Id accountId) { return [ SELECT Id, Name, Email, Phone FROM Contact WHERE AccountId = :accountId WITH SECURITY_ENFORCED LIMIT 100 ]; } }
Apex triggers must be bulkified to handle 200+ records per transaction. Use handler pattern for separation of concerns, testability, and recursion prevention.
// AccountTrigger.trigger trigger AccountTrigger on Account ( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { new AccountTriggerHandler().run(); }
// TriggerHandler.cls (base class) public virtual class TriggerHandler { // Recursion prevention private static Set executedHandlers = new Set();
public void run() { String handlerName = String.valueOf(this).split(':')[0];
// Prevent recursion
String contextKey = handlerName + '_' + Trigger.operationType;
if (executedHandlers.contains(contextKey)) {
return;
}
executedHandlers.add(contextKey);
switch on Trigger.operationType {
when BEFORE_INSERT { this.beforeInsert(); }
when BEFORE_UPDATE { this.beforeUpdate(); }
when BEFORE_DELETE { this.beforeDelete(); }
when AFTER_INSERT { this.afterInsert(); }
when AFTER_UPDATE { this.afterUpdate(); }
when AFTER_DELETE { this.afterDelete(); }
when AFTER_UNDELETE { this.afterUndelete(); }
}
}
// Override in child classes protected virtual void beforeInsert() {} protected virtual void beforeUpdate() {} protected virtual void beforeDelete() {} protected virtual void afterInsert() {} protected virtual void afterUpdate() {} protected virtual void afterDelete() {} protected virtual void afterUndelete() {} }
// AccountTriggerHandler.cls public class AccountTriggerHandler extends TriggerHandler { private List newAccounts; private List oldAccounts; private Map<Id, Account> newMap; private Map<Id, Account> oldMap;
public AccountTriggerHandler() { this.newAccounts = (List) Trigger.new; this.oldAccounts = (List) Trigger.old; this.newMap = (Map<Id, Account>) Trigger.newMap; this.oldMap = (Map<Id, Account>) Trigger.oldMap; }
protected override void afterInsert() { createDefaultContacts(); notifySlack(); }
protected override void afterUpdate() { handleIndustryChange(); }
// BULKIFIED: Query once, update once private void createDefaultContacts() { List contactsToInsert = new List();
for (Account acc : newAccounts) {
if (acc.Type == 'Prospect') {
contactsToInsert.add(new Contact(
AccountId = acc.Id,
LastName = 'Primary Contact',
Email = 'contact@' + acc.Website
));
}
}
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert; // Single DML for all
}
}
private void handleIndustryChange() { Set changedAccountIds = new Set();
for (Account acc : newAccounts) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Industry != oldAcc.Industry) {
changedAccountIds.add(acc.Id);
}
}
if (!changedAccountIds.isEmpty()) {
// Queue async processing for heavy work
System.enqueueJob(new IndustryChangeQueueable(changedAccountIds));
}
}
private void notifySlack() { // Offload callouts to async List accountIds = new List(newMap.keySet()); System.enqueueJob(new SlackNotificationQueueable(accountIds)); } }
Use Queueable Apex for async processing with support for non-primitive types, monitoring via AsyncApexJob, and job chaining. Limit: 50 jobs per transaction, 1 child job when chaining.
// IndustryChangeQueueable.cls public class IndustryChangeQueueable implements Queueable, Database.AllowsCallouts { private Set accountIds; private Integer retryCount;
public IndustryChangeQueueable(Set accountIds) { this(accountIds, 0); }
public IndustryChangeQueueable(Set accountIds, Integer retryCount) { this.accountIds = accountIds; this.retryCount = retryCount; }
public void execute(QueueableContext context) { try { // Query with fresh data List accounts = [ SELECT Id, Name, Industry, OwnerId FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED ];
// Process and make callout
for (Account acc : accounts) {
syncToExternalSystem(acc);
}
// Update records
updateRelatedOpportunities(accountIds);
} catch (Exception e) {
handleError(e);
}
}
private void syncToExternalSystem(Account acc) { HttpRequest req = new HttpRequest(); req.setEndpoint('callout:ExternalCRM/accounts'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/json'); req.setBody(JSON.serialize(new Map<String, Object>{ 'salesforceId' => acc.Id, 'name' => acc.Name, 'industry' => acc.Industry }));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() != 200 && res.getStatusCode() != 201) {
throw new CalloutException('Sync failed: ' + res.getBody());
}
}
private void updateRelatedOpportunities(Set accIds) { List oppsToUpdate = [ SELECT Id, Industry__c, AccountId FROM Opportunity WHERE AccountId IN :accIds WITH SECURITY_ENFORCED ];
Map<Id, Account> accountMap = new Map<Id, Account>([
SELECT Id, Industry FROM Account WHERE Id IN :accIds
]);
for (Opportunity opp : oppsToUpdate) {
opp.Industry__c = accountMap.get(opp.AccountId).Industry;
}
if (!oppsToUpdate.isEmpty()) {
update oppsToUpdate;
}
}
private void handleError(Exception e) { // Log error System.debug(LoggingLevel.ERROR, 'Queueable failed: ' + e.getMessage());
// Retry with exponential backoff (max 3 retries)
if (retryCount < 3) {
// Chain new job for retry
System.enqueueJob(new IndustryChangeQueueable(accountIds, retryCount + 1));
} else {
// Create error record for monitoring
insert new Integration_Error__c(
Type__c = 'Industry Sync',
Message__c = e.getMessage(),
Stack_Trace__c = e.getStackTraceString(),
Record_Ids__c = String.join(new List<Id>(accountIds), ',')
);
}
} }
External integrations use Connected Apps with OAuth 2.0. JWT Bearer flow for server-to-server, Web Server flow for user-facing apps. Always use Named Credentials for secure callout configuration.
// Node.js - JWT Bearer Flow (server-to-server) import jwt from 'jsonwebtoken'; import fs from 'fs';
class SalesforceClient { private accessToken: string | null = null; private instanceUrl: string | null = null; private tokenExpiry: number = 0;
constructor( private clientId: string, private username: string, private privateKeyPath: string, private loginUrl: string = 'https://login.salesforce.com' ) {}
async authenticate(): Promise { // Check if token is still valid (5 min buffer) if (this.accessToken && Date.now() < this.tokenExpiry - 300000) { return; }
const privateKey = fs.readFileSync(this.privateKeyPath, 'utf8');
// Create JWT assertion
const claim = {
iss: this.clientId,
sub: this.username,
aud: this.loginUrl,
exp: Math.floor(Date.now() / 1000) + 300 // 5 minutes
};
const assertion = jwt.sign(claim, privateKey, { algorithm: 'RS256' });
// Exchange JWT for access token
const response = await fetch(`${this.loginUrl}/services/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Auth failed: ${error.error_description}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.instanceUrl = data.instance_url;
this.tokenExpiry = Date.now() + 7200000; // 2 hours
}
async query(soql: string): Promise { await this.authenticate();
const response = await fetch(
`${this.instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(soql)}`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
await this.handleError(response);
}
return response.json();
}
async createRecord(sobject: string, data: object): Promise { await this.authenticate();
const response = await fetch(
`${this.instanceUrl}/services/data/v59.0/sobjects/${sobject}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}
);
if (!response.ok) {
await this.handleError(response);
}
return response.json();
}
private async handleError(response: Response): Promise { const error = await response.json();
if (response.status === 401) {
// Token expired, clear and retry
this.accessToken = null;
throw new Error('Session expired, retry required');
}
throw new Error(`API Error: ${JSON.stringify(error)}`);
} }
// Usage const sf = new SalesforceClient( process.env.SF_CLIENT_ID!, process.env.SF_USERNAME!, './certificates/server.key' );
const accounts = await sf.query( "SELECT Id, Name FROM Account WHERE CreatedDate = TODAY" );
Use Bulk API 2.0 for operations on 10K+ records. Asynchronous processing with job-based workflow. Part of REST API with streamlined interface compared to original Bulk API.
// Node.js - Bulk API 2.0 insert class SalesforceBulkClient extends SalesforceClient {
async bulkInsert(sobject: string, records: object[]): Promise { await this.authenticate();
// Step 1: Create job
const job = await this.createBulkJob(sobject, 'insert');
try {
// Step 2: Upload data (CSV format)
await this.uploadJobData(job.id, records);
// Step 3: Close job to start processing
await this.closeJob(job.id);
// Step 4: Poll for completion
return await this.waitForJobCompletion(job.id);
} catch (error) {
// Abort job on error
await this.abortJob(job.id);
throw error;
}
}
private async createBulkJob(sobject: string, operation: string): Promise {
const response = await fetch(
${this.instanceUrl}/services/data/v59.0/jobs/ingest,
{
method: 'POST',
headers: {
'Authorization': Bearer ${this.accessToken},
'Content-Type': 'application/json'
},
body: JSON.stringify({
object: sobject,
operation,
contentType: 'CSV',
lineEnding: 'LF'
})
}
);
return response.json();
}
private async uploadJobData(jobId: string, records: object[]): Promise { // Convert to CSV const csv = this.recordsToCSV(records);
await fetch(
`${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}/batches`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'text/csv'
},
body: csv
}
);
}
private async closeJob(jobId: string): Promise {
await fetch(
${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId},
{
method: 'PATCH',
headers: {
'Authorization': Bearer ${this.accessToken},
'Content-Type': 'application/json'
},
body: JSON.stringify({ state: 'UploadComplete' })
}
);
}
private async waitForJobCompletion(jobId: string): Promise { const maxWaitTime = 10 * 60 * 1000; // 10 minutes const pollInterval = 5000; // 5 seconds const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
const response = await fetch(
`${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}`,
{
headers: { 'Authorization': `Bearer ${this.accessToken}` }
}
);
const job = await response.json();
if (job.state === 'JobComplete') {
// Get results
return {
success: job.numberRecordsProcessed - job.numberRecordsFailed,
failed: job.numberRecordsFailed,
failedResults: job.numberRecordsFailed > 0
? await this.getFailedResults(jobId)
: []
};
}
if (job.state === 'Failed' || job.state === 'Aborted') {
throw new Error(`Bulk job failed: ${job.state}`);
}
await new Promise(r => setTimeout(r, pollInterval));
}
throw new Error('Bulk job timeout');
}
private async getFailedResults(jobId: string): Promise<any[]> {
const response = await fetch(
${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}/failedResults,
{
headers: { 'Authorization': Bearer ${this.accessToken} }
}
);
const csv = await response.text();
return this.parseCSV(csv);
}
private recordsToCSV(records: object[]): string { if (records.length === 0) return '';
const headers = Object.keys(records[0]);
const rows = records.map(r =>
headers.map(h => this.escapeCSV(r[h])).join(',')
);
return [headers.join(','), ...rows].join('\n');
}
private escapeCSV(value: any): string {
if (value === null || value === undefined) return '';
const str = String(value);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return "${str.replace(/"/g, '""')}";
}
return str;
}
}
Source-driven development with disposable scratch orgs for isolated testing. Scratch orgs exist 7-30 days and can be created throughout the day, unlike sandbox refresh limits.
// project-scratch-def.json - Scratch org definition { "orgName": "MyApp Dev Org", "edition": "Developer", "features": ["EnableSetPasswordInApi", "Communities"], "settings": { "lightningExperienceSettings": { "enableS1DesktopEnabled": true }, "mobileSettings": { "enableS1EncryptedStoragePref2": false }, "securitySettings": { "passwordPolicies": { "enableSetPasswordInApi": true } } } }
// sfdx-project.json - Project configuration { "packageDirectories": [ { "path": "force-app", "default": true, "package": "MyPackage", "versionName": "ver 1.0", "versionNumber": "1.0.0.NEXT", "dependencies": [ { "package": "SomePackage@2.0.0" } ] } ], "namespace": "myns", "sfdcLoginUrl": "https://login.salesforce.com", "sourceApiVersion": "59.0" }
sf org create scratch
--definition-file config/project-scratch-def.json
--alias myapp-dev
--duration-days 7
--set-default
sf project deploy start --target-org myapp-dev
sf org assign permset --name MyApp_Admin --target-org myapp-dev
sf data import tree --plan data/sample-data-plan.json --target-org myapp-dev
sf org open --target-org myapp-dev
sf apex run test
--code-coverage
--result-format human
--wait 10
--target-org myapp-dev
sf project retrieve start --target-org myapp-dev
2GP replaces 1GP with source-driven, modular packaging. Requires Dev Hub with 2GP enabled, namespace linked, and 75% code coverage for promoted packages.
sf package create
--name "MyManagedPackage"
--package-type Managed
--path force-app
--target-dev-hub DevHub
sf package version create
--package "MyManagedPackage"
--installation-key-bypass
--wait 30
--code-coverage
--target-dev-hub DevHub
sf package version list --packages "MyManagedPackage" --target-dev-hub DevHub
sf package version promote
--package "MyManagedPackage@1.0.0-1"
--target-dev-hub DevHub
sf package install
--package "MyManagedPackage@1.0.0-1"
--target-org MySandbox
--wait 20
name: Salesforce CI
on: push: branches: [main, develop] pull_request: branches: [main]
jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Install Salesforce CLI
run: npm install -g @salesforce/cli
- name: Authenticate Dev Hub
run: |
echo "${{ secrets.SFDX_AUTH_URL }}" > auth.txt
sf org login sfdx-url --sfdx-url-file auth.txt --alias DevHub --set-default-dev-hub
- name: Create Scratch Org
run: |
sf org create scratch \
--definition-file config/project-scratch-def.json \
--alias ci-scratch \
--duration-days 1 \
--set-default
- name: Deploy Source
run: sf project deploy start --target-org ci-scratch
- name: Run Tests
run: |
sf apex run test \
--code-coverage \
--result-format human \
--wait 20 \
--target-org ci-scratch
- name: Delete Scratch Org
if: always()
run: sf org delete scratch --target-org ci-scratch --no-prompt
Severity: CRITICAL
Severity: HIGH
Severity: MEDIUM
Severity: HIGH
Severity: CRITICAL
Severity: HIGH
Severity: HIGH
Severity: CRITICAL
Severity: MEDIUM
Severity: MEDIUM
Severity: ERROR
SOQL in loops causes governor limit exceptions with bulk data
Message: SOQL query inside loop. Query once outside the loop and use a Map.
Severity: ERROR
DML in loops hits 150 statement limit
Message: DML operation inside loop. Collect records and perform single DML outside loop.
Severity: ERROR
Synchronous triggers cannot make callouts
Message: Callout in trigger. Use @future(callout=true) or Queueable with Database.AllowsCallouts.
Severity: ERROR
Dynamic SOQL with string concatenation is vulnerable
Message: Dynamic SOQL with concatenation. Use bind variables or String.escapeSingleQuotes().
Severity: WARNING
SOQL should enforce FLS/CRUD permissions
Message: SOQL without security enforcement. Add WITH SECURITY_ENFORCED.
Severity: WARNING
Record IDs differ between orgs
Message: Hardcoded Salesforce ID. Query by DeveloperName or ExternalId instead.
Severity: ERROR
Credentials must use Named Credentials or Custom Metadata
Message: Hardcoded credentials. Use Named Credentials or Custom Metadata.
Severity: WARNING
LWC uses shadow DOM, direct manipulation breaks encapsulation
Message: Direct DOM access in LWC. Use this.template.querySelector() or data binding.
Severity: INFO
Complex object properties need @track for reactivity
Message: Object assignment may need @track for reactivity (post-Spring '20 objects are auto-tracked).
Severity: WARNING
Cached wire data becomes stale after updates
Message: DML after @wire without refreshApex. Data may be stale.