scc-universal 1.1.0
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/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# LWC Component Example
|
|
2
|
+
|
|
3
|
+
Sample Lightning Web Component with wire service, Apex calls, and Jest tests.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
force-app/main/default/lwc/
|
|
9
|
+
accountList/
|
|
10
|
+
accountList.html
|
|
11
|
+
accountList.js
|
|
12
|
+
accountList.js-meta.xml
|
|
13
|
+
accountList.css
|
|
14
|
+
__tests__/
|
|
15
|
+
accountList.test.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Component (HTML)
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<template>
|
|
22
|
+
<lightning-card title="Accounts" icon-name="standard:account">
|
|
23
|
+
<template if:true={accounts.data}>
|
|
24
|
+
<lightning-datatable
|
|
25
|
+
key-field="Id"
|
|
26
|
+
data={accounts.data}
|
|
27
|
+
columns={columns}
|
|
28
|
+
onrowaction={handleRowAction}>
|
|
29
|
+
</lightning-datatable>
|
|
30
|
+
</template>
|
|
31
|
+
<template if:true={accounts.error}>
|
|
32
|
+
<p class="slds-text-color_error">Error loading accounts: {errorMessage}</p>
|
|
33
|
+
</template>
|
|
34
|
+
<template if:false={accounts.data}>
|
|
35
|
+
<lightning-spinner alternative-text="Loading"></lightning-spinner>
|
|
36
|
+
</template>
|
|
37
|
+
</lightning-card>
|
|
38
|
+
</template>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Component (JS)
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { LightningElement, wire } from 'lwc';
|
|
45
|
+
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
|
|
46
|
+
|
|
47
|
+
const COLUMNS = [
|
|
48
|
+
{ label: 'Name', fieldName: 'Name', type: 'text' },
|
|
49
|
+
{ label: 'Industry', fieldName: 'Industry', type: 'text' },
|
|
50
|
+
{ label: 'Revenue', fieldName: 'AnnualRevenue', type: 'currency' },
|
|
51
|
+
{
|
|
52
|
+
type: 'action',
|
|
53
|
+
typeAttributes: { rowActions: [{ label: 'View', name: 'view' }] }
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export default class AccountList extends LightningElement {
|
|
58
|
+
columns = COLUMNS;
|
|
59
|
+
|
|
60
|
+
@wire(getAccounts)
|
|
61
|
+
accounts;
|
|
62
|
+
|
|
63
|
+
get errorMessage() {
|
|
64
|
+
return this.accounts?.error?.body?.message || 'Unknown error';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
handleRowAction(event) {
|
|
68
|
+
const { action, row } = event.detail;
|
|
69
|
+
if (action.name === 'view') {
|
|
70
|
+
this.dispatchEvent(new CustomEvent('viewaccount', { detail: { accountId: row.Id } }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Component Metadata
|
|
77
|
+
|
|
78
|
+
```xml
|
|
79
|
+
<!-- accountList.js-meta.xml -->
|
|
80
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
81
|
+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
82
|
+
<apiVersion>66.0</apiVersion>
|
|
83
|
+
<isExposed>true</isExposed>
|
|
84
|
+
<targets>
|
|
85
|
+
<target>lightning__RecordPage</target>
|
|
86
|
+
<target>lightning__AppPage</target>
|
|
87
|
+
<target>lightning__HomePage</target>
|
|
88
|
+
</targets>
|
|
89
|
+
<targetConfigs>
|
|
90
|
+
<targetConfig targets="lightning__RecordPage">
|
|
91
|
+
<objects>
|
|
92
|
+
<object>Account</object>
|
|
93
|
+
</objects>
|
|
94
|
+
</targetConfig>
|
|
95
|
+
</targetConfigs>
|
|
96
|
+
</LightningComponentBundle>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Jest Tests
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
import { createElement } from 'lwc';
|
|
103
|
+
import AccountList from 'c/accountList';
|
|
104
|
+
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
|
|
105
|
+
|
|
106
|
+
// Standard @salesforce/sfdx-lwc-jest mock pattern
|
|
107
|
+
jest.mock(
|
|
108
|
+
'@salesforce/apex/AccountController.getAccounts',
|
|
109
|
+
() => ({ default: jest.fn() }),
|
|
110
|
+
{ virtual: true }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const MOCK_ACCOUNTS = [
|
|
114
|
+
{ Id: '001xx001', Name: 'Acme', Industry: 'Tech', AnnualRevenue: 100000 },
|
|
115
|
+
{ Id: '001xx002', Name: 'Global', Industry: 'Finance', AnnualRevenue: 500000 },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
describe('c-account-list', () => {
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
while (document.body.firstChild) {
|
|
121
|
+
document.body.removeChild(document.body.firstChild);
|
|
122
|
+
}
|
|
123
|
+
jest.clearAllMocks();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders datatable when data is available', async () => {
|
|
127
|
+
getAccounts.mockResolvedValue(MOCK_ACCOUNTS);
|
|
128
|
+
const element = createElement('c-account-list', { is: AccountList });
|
|
129
|
+
document.body.appendChild(element);
|
|
130
|
+
|
|
131
|
+
await Promise.resolve();
|
|
132
|
+
await Promise.resolve();
|
|
133
|
+
|
|
134
|
+
const datatable = element.shadowRoot.querySelector('lightning-datatable');
|
|
135
|
+
expect(datatable).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('shows error message on failure', async () => {
|
|
139
|
+
getAccounts.mockRejectedValue({ body: { message: 'Test error' } });
|
|
140
|
+
const element = createElement('c-account-list', { is: AccountList });
|
|
141
|
+
document.body.appendChild(element);
|
|
142
|
+
|
|
143
|
+
await Promise.resolve();
|
|
144
|
+
await Promise.resolve();
|
|
145
|
+
|
|
146
|
+
const error = element.shadowRoot.querySelector('.slds-text-color_error');
|
|
147
|
+
expect(error).not.toBeNull();
|
|
148
|
+
expect(error.textContent).toContain('Test error');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('dispatches viewaccount event on row action', async () => {
|
|
152
|
+
getAccounts.mockResolvedValue(MOCK_ACCOUNTS);
|
|
153
|
+
const element = createElement('c-account-list', { is: AccountList });
|
|
154
|
+
const handler = jest.fn();
|
|
155
|
+
element.addEventListener('viewaccount', handler);
|
|
156
|
+
document.body.appendChild(element);
|
|
157
|
+
|
|
158
|
+
await Promise.resolve();
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
|
|
161
|
+
const datatable = element.shadowRoot.querySelector('lightning-datatable');
|
|
162
|
+
datatable.dispatchEvent(
|
|
163
|
+
new CustomEvent('rowaction', {
|
|
164
|
+
detail: { action: { name: 'view' }, row: MOCK_ACCOUNTS[0] }
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(handler.mock.calls[0][0].detail.accountId).toBe('001xx001');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('shows spinner when data is loading', () => {
|
|
173
|
+
const element = createElement('c-account-list', { is: AccountList });
|
|
174
|
+
document.body.appendChild(element);
|
|
175
|
+
|
|
176
|
+
const spinner = element.shadowRoot.querySelector('lightning-spinner');
|
|
177
|
+
expect(spinner).not.toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
```
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Platform Events with Change Data Capture
|
|
2
|
+
|
|
3
|
+
Implementing Platform Events for real-time event-driven architecture and subscribing to Change Data Capture events in LWC.
|
|
4
|
+
|
|
5
|
+
## When to Use This Pattern
|
|
6
|
+
|
|
7
|
+
- Building real-time notifications between Salesforce components or external systems
|
|
8
|
+
- Reacting to record changes across the org without triggers on every object
|
|
9
|
+
- Decoupling publishers from subscribers for loosely coupled integrations
|
|
10
|
+
- Streaming data changes to Lightning components for live UI updates
|
|
11
|
+
|
|
12
|
+
## Structure
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
force-app/main/default/
|
|
16
|
+
objects/
|
|
17
|
+
Order_Status_Event__e/
|
|
18
|
+
Order_Status_Event__e.object-meta.xml # Platform Event definition
|
|
19
|
+
fields/
|
|
20
|
+
Order_Id__c.field-meta.xml
|
|
21
|
+
Status__c.field-meta.xml
|
|
22
|
+
Message__c.field-meta.xml
|
|
23
|
+
classes/
|
|
24
|
+
OrderEventPublisher.cls # Publishes events from Apex
|
|
25
|
+
OrderEventPublisher_Test.cls
|
|
26
|
+
OrderEventSubscriber.cls # Trigger subscriber
|
|
27
|
+
OrderEventSubscriber_Test.cls
|
|
28
|
+
triggers/
|
|
29
|
+
OrderStatusEventTrigger.trigger # Event trigger
|
|
30
|
+
lwc/
|
|
31
|
+
orderStatusMonitor/ # LWC subscriber via empApi
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Platform Event Definition
|
|
35
|
+
|
|
36
|
+
```xml
|
|
37
|
+
<!-- Order_Status_Event__e.object-meta.xml -->
|
|
38
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
39
|
+
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
40
|
+
<deploymentStatus>Deployed</deploymentStatus>
|
|
41
|
+
<description>Published when an order status changes. Subscribers include
|
|
42
|
+
UI components and integration middleware.</description>
|
|
43
|
+
<eventType>HighVolume</eventType>
|
|
44
|
+
<label>Order Status Event</label>
|
|
45
|
+
<pluralLabel>Order Status Events</pluralLabel>
|
|
46
|
+
<publishBehavior>PublishAfterCommit</publishBehavior>
|
|
47
|
+
</CustomObject>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```xml
|
|
51
|
+
<!-- Order_Id__c.field-meta.xml -->
|
|
52
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
54
|
+
<fullName>Order_Id__c</fullName>
|
|
55
|
+
<label>Order ID</label>
|
|
56
|
+
<length>18</length>
|
|
57
|
+
<type>Text</type>
|
|
58
|
+
<required>true</required>
|
|
59
|
+
<description>The Salesforce ID of the order that changed status</description>
|
|
60
|
+
</CustomField>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Publishing Events from Apex
|
|
64
|
+
|
|
65
|
+
```apex
|
|
66
|
+
public with sharing class OrderEventPublisher {
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Publishes order status change events. Uses PublishAfterCommit so events
|
|
70
|
+
* are only delivered if the enclosing transaction succeeds.
|
|
71
|
+
*/
|
|
72
|
+
public static void publishStatusChange(Id orderId, String newStatus, String message) {
|
|
73
|
+
Order_Status_Event__e event = new Order_Status_Event__e(
|
|
74
|
+
Order_Id__c = orderId,
|
|
75
|
+
Status__c = newStatus,
|
|
76
|
+
Message__c = message
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
Database.SaveResult result = EventBus.publish(event);
|
|
80
|
+
|
|
81
|
+
if (!result.isSuccess()) {
|
|
82
|
+
for (Database.Error err : result.getErrors()) {
|
|
83
|
+
System.debug(LoggingLevel.ERROR,
|
|
84
|
+
'Order event publish failed: ' + err.getStatusCode() + ' - ' + err.getMessage()
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Bulk publish for batch processes. Publishes up to 10,000 events per call.
|
|
92
|
+
*/
|
|
93
|
+
public static List<Database.SaveResult> publishBulkStatusChanges(
|
|
94
|
+
List<Order_Status_Event__e> events
|
|
95
|
+
) {
|
|
96
|
+
return EventBus.publish(events);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Subscribing via Apex Trigger
|
|
102
|
+
|
|
103
|
+
```apex
|
|
104
|
+
// OrderStatusEventTrigger.trigger
|
|
105
|
+
trigger OrderStatusEventTrigger on Order_Status_Event__e (after insert) {
|
|
106
|
+
OrderEventSubscriber.handleEvents(Trigger.new);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```apex
|
|
111
|
+
public with sharing class OrderEventSubscriber {
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Processes incoming order status events. Updates related records and
|
|
115
|
+
* creates tasks for critical status changes. Uses
|
|
116
|
+
* EventBus.TriggerContext.currentContext().setResumeCheckpoint to
|
|
117
|
+
* handle large event volumes gracefully.
|
|
118
|
+
*/
|
|
119
|
+
public static void handleEvents(List<Order_Status_Event__e> events) {
|
|
120
|
+
Set<String> orderIdStrings = new Set<String>();
|
|
121
|
+
Map<String, Order_Status_Event__e> latestByOrder = new Map<String, Order_Status_Event__e>();
|
|
122
|
+
|
|
123
|
+
for (Order_Status_Event__e evt : events) {
|
|
124
|
+
orderIdStrings.add(evt.Order_Id__c);
|
|
125
|
+
// Keep only the latest event per order (events arrive in order)
|
|
126
|
+
latestByOrder.put(evt.Order_Id__c, evt);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Query orders to validate IDs exist — Order_Id__c is Text, so we must
|
|
130
|
+
// verify the referenced records are real before using them as WhatId
|
|
131
|
+
Map<Id, Order> ordersById = new Map<Id, Order>(
|
|
132
|
+
[SELECT Id FROM Order WHERE Id IN :orderIdStrings]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Create follow-up tasks for failed orders
|
|
136
|
+
List<Task> tasks = new List<Task>();
|
|
137
|
+
for (Order_Status_Event__e evt : latestByOrder.values()) {
|
|
138
|
+
if (evt.Status__c == 'Failed') {
|
|
139
|
+
// Safely convert Text to Id — skip if the value is invalid
|
|
140
|
+
Id orderId;
|
|
141
|
+
try {
|
|
142
|
+
orderId = Id.valueOf(evt.Order_Id__c);
|
|
143
|
+
} catch (StringException e) {
|
|
144
|
+
System.debug(LoggingLevel.ERROR,
|
|
145
|
+
'Invalid Order ID in event: ' + evt.Order_Id__c);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (ordersById.containsKey(orderId)) {
|
|
150
|
+
tasks.add(new Task(
|
|
151
|
+
Subject = 'Order Failed: ' + evt.Order_Id__c,
|
|
152
|
+
Description = evt.Message__c,
|
|
153
|
+
WhatId = orderId,
|
|
154
|
+
Priority = 'High',
|
|
155
|
+
Status = 'Not Started',
|
|
156
|
+
ActivityDate = Date.today().addDays(1)
|
|
157
|
+
));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!tasks.isEmpty()) {
|
|
163
|
+
insert tasks;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Set checkpoint for replay on large batches
|
|
167
|
+
EventBus.TriggerContext.currentContext().setResumeCheckpoint(
|
|
168
|
+
events[events.size() - 1].ReplayId
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Subscribing in LWC via empApi
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<!-- orderStatusMonitor.html -->
|
|
178
|
+
<template>
|
|
179
|
+
<lightning-card title="Order Status Monitor" icon-name="standard:orders">
|
|
180
|
+
<div class="slds-p-around_small">
|
|
181
|
+
<template if:true={isSubscribed}>
|
|
182
|
+
<lightning-badge label="Live" class="slds-m-bottom_small slds-theme_success"></lightning-badge>
|
|
183
|
+
</template>
|
|
184
|
+
<template if:false={isSubscribed}>
|
|
185
|
+
<lightning-badge label="Disconnected" class="slds-m-bottom_small"></lightning-badge>
|
|
186
|
+
<lightning-button label="Connect" onclick={handleSubscribe} variant="brand" class="slds-m-left_small"></lightning-button>
|
|
187
|
+
</template>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<template if:true={hasEvents}>
|
|
191
|
+
<lightning-datatable
|
|
192
|
+
key-field="replayId"
|
|
193
|
+
data={events}
|
|
194
|
+
columns={columns}
|
|
195
|
+
hide-checkbox-column>
|
|
196
|
+
</lightning-datatable>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<template if:false={hasEvents}>
|
|
200
|
+
<div class="slds-p-around_medium slds-text-align_center slds-text-color_weak">
|
|
201
|
+
Waiting for order status events...
|
|
202
|
+
</div>
|
|
203
|
+
</template>
|
|
204
|
+
</lightning-card>
|
|
205
|
+
</template>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
// orderStatusMonitor.js
|
|
210
|
+
import { LightningElement } from 'lwc';
|
|
211
|
+
import {
|
|
212
|
+
subscribe,
|
|
213
|
+
unsubscribe,
|
|
214
|
+
onError,
|
|
215
|
+
setDebugFlag,
|
|
216
|
+
isEmpEnabled
|
|
217
|
+
} from 'lightning/empApi';
|
|
218
|
+
|
|
219
|
+
const CHANNEL = '/event/Order_Status_Event__e';
|
|
220
|
+
const MAX_EVENTS = 50;
|
|
221
|
+
|
|
222
|
+
const COLUMNS = [
|
|
223
|
+
{ label: 'Order ID', fieldName: 'orderId', type: 'text' },
|
|
224
|
+
{ label: 'Status', fieldName: 'status', type: 'text' },
|
|
225
|
+
{ label: 'Message', fieldName: 'message', type: 'text' },
|
|
226
|
+
{ label: 'Time', fieldName: 'timestamp', type: 'text' }
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
export default class OrderStatusMonitor extends LightningElement {
|
|
230
|
+
subscription = null;
|
|
231
|
+
events = [];
|
|
232
|
+
columns = COLUMNS;
|
|
233
|
+
|
|
234
|
+
get isSubscribed() {
|
|
235
|
+
return this.subscription != null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
get hasEvents() {
|
|
239
|
+
return this.events.length > 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
connectedCallback() {
|
|
243
|
+
this.registerErrorListener();
|
|
244
|
+
this.handleSubscribe();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
disconnectedCallback() {
|
|
248
|
+
this.handleUnsubscribe();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
handleSubscribe() {
|
|
252
|
+
// Subscribe with replay -1 (new events only) or -2 (all retained events)
|
|
253
|
+
const replayId = -1;
|
|
254
|
+
|
|
255
|
+
subscribe(CHANNEL, replayId, (response) => {
|
|
256
|
+
this.handleEvent(response);
|
|
257
|
+
}).then((sub) => {
|
|
258
|
+
this.subscription = sub;
|
|
259
|
+
console.log('Subscribed to ' + CHANNEL);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handleUnsubscribe() {
|
|
264
|
+
if (this.subscription) {
|
|
265
|
+
unsubscribe(this.subscription, () => {
|
|
266
|
+
this.subscription = null;
|
|
267
|
+
console.log('Unsubscribed from ' + CHANNEL);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
handleEvent(response) {
|
|
273
|
+
const payload = response.data.payload;
|
|
274
|
+
const newEvent = {
|
|
275
|
+
replayId: response.data.event.replayId,
|
|
276
|
+
orderId: payload.Order_Id__c,
|
|
277
|
+
status: payload.Status__c,
|
|
278
|
+
message: payload.Message__c,
|
|
279
|
+
timestamp: new Date(payload.CreatedDate).toLocaleString()
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Prepend new event; keep only the most recent MAX_EVENTS
|
|
283
|
+
this.events = [newEvent, ...this.events].slice(0, MAX_EVENTS);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
registerErrorListener() {
|
|
287
|
+
onError((error) => {
|
|
288
|
+
console.error('empApi error: ', JSON.stringify(error));
|
|
289
|
+
// Reset subscription state so user can reconnect
|
|
290
|
+
this.subscription = null;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Change Data Capture Subscription
|
|
297
|
+
|
|
298
|
+
Change Data Capture (CDC) publishes events automatically when records are created, updated, deleted, or undeleted. No custom event definition is needed.
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
// accountChangeMonitor.js
|
|
302
|
+
import { LightningElement } from 'lwc';
|
|
303
|
+
import { subscribe, unsubscribe, onError } from 'lightning/empApi';
|
|
304
|
+
|
|
305
|
+
// CDC channel format: /data/<ObjectName>ChangeEvent
|
|
306
|
+
const CDC_CHANNEL = '/data/AccountChangeEvent';
|
|
307
|
+
|
|
308
|
+
export default class AccountChangeMonitor extends LightningElement {
|
|
309
|
+
subscription = null;
|
|
310
|
+
changes = [];
|
|
311
|
+
|
|
312
|
+
connectedCallback() {
|
|
313
|
+
this.registerErrorListener();
|
|
314
|
+
subscribe(CDC_CHANNEL, -1, (response) => {
|
|
315
|
+
this.handleChangeEvent(response);
|
|
316
|
+
}).then((sub) => {
|
|
317
|
+
this.subscription = sub;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
disconnectedCallback() {
|
|
322
|
+
if (this.subscription) {
|
|
323
|
+
unsubscribe(this.subscription, () => {
|
|
324
|
+
this.subscription = null;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
handleChangeEvent(response) {
|
|
330
|
+
const header = response.data.payload.ChangeEventHeader;
|
|
331
|
+
const change = {
|
|
332
|
+
id: Date.now(),
|
|
333
|
+
recordIds: header.recordIds.join(', '),
|
|
334
|
+
changeType: header.changeType, // CREATE, UPDATE, DELETE, UNDELETE
|
|
335
|
+
changedFields: header.changedFields.join(', '),
|
|
336
|
+
commitUser: header.commitUser,
|
|
337
|
+
timestamp: new Date(header.commitTimestamp).toLocaleString()
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this.changes = [change, ...this.changes].slice(0, 100);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
registerErrorListener() {
|
|
344
|
+
onError((error) => {
|
|
345
|
+
console.error('CDC error: ', JSON.stringify(error));
|
|
346
|
+
this.subscription = null;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Error Handling and Replay
|
|
353
|
+
|
|
354
|
+
```apex
|
|
355
|
+
// Handling publish failures with retry
|
|
356
|
+
public with sharing class EventPublishHelper {
|
|
357
|
+
|
|
358
|
+
private static final Integer MAX_RETRIES = 3;
|
|
359
|
+
|
|
360
|
+
public static void publishWithRetry(List<Order_Status_Event__e> events) {
|
|
361
|
+
Integer attempts = 0;
|
|
362
|
+
List<Order_Status_Event__e> failedEvents = new List<Order_Status_Event__e>(events);
|
|
363
|
+
|
|
364
|
+
while (!failedEvents.isEmpty() && attempts < MAX_RETRIES) {
|
|
365
|
+
attempts++;
|
|
366
|
+
List<Database.SaveResult> results = EventBus.publish(failedEvents);
|
|
367
|
+
failedEvents = new List<Order_Status_Event__e>();
|
|
368
|
+
|
|
369
|
+
for (Integer i = 0; i < results.size(); i++) {
|
|
370
|
+
if (!results[i].isSuccess()) {
|
|
371
|
+
failedEvents.add(events[i]);
|
|
372
|
+
for (Database.Error err : results[i].getErrors()) {
|
|
373
|
+
System.debug(LoggingLevel.WARN,
|
|
374
|
+
'Publish attempt ' + attempts + ' failed for event ' + i
|
|
375
|
+
+ ': ' + err.getMessage()
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!failedEvents.isEmpty()) {
|
|
383
|
+
System.debug(LoggingLevel.ERROR,
|
|
384
|
+
failedEvents.size() + ' events failed after ' + MAX_RETRIES + ' attempts'
|
|
385
|
+
);
|
|
386
|
+
// Log to a custom object or send an alert
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Test Class
|
|
393
|
+
|
|
394
|
+
```apex
|
|
395
|
+
@IsTest
|
|
396
|
+
private class OrderEventPublisher_Test {
|
|
397
|
+
|
|
398
|
+
@IsTest
|
|
399
|
+
static void testPublishSingleEvent() {
|
|
400
|
+
Test.startTest();
|
|
401
|
+
OrderEventPublisher.publishStatusChange(
|
|
402
|
+
'801xx000000001AAA',
|
|
403
|
+
'Shipped',
|
|
404
|
+
'Order has been shipped via FedEx'
|
|
405
|
+
);
|
|
406
|
+
Test.stopTest();
|
|
407
|
+
|
|
408
|
+
// Verify event was published (query the event bus in test context)
|
|
409
|
+
// Platform Events published in tests are immediately available after Test.stopTest()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@IsTest
|
|
413
|
+
static void testPublishBulkEvents() {
|
|
414
|
+
List<Order_Status_Event__e> events = new List<Order_Status_Event__e>();
|
|
415
|
+
for (Integer i = 0; i < 200; i++) {
|
|
416
|
+
events.add(new Order_Status_Event__e(
|
|
417
|
+
Order_Id__c = '801xx00000000' + String.valueOf(i).leftPad(4, '0'),
|
|
418
|
+
Status__c = 'Processing',
|
|
419
|
+
Message__c = 'Bulk event ' + i
|
|
420
|
+
));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
Test.startTest();
|
|
424
|
+
List<Database.SaveResult> results = OrderEventPublisher.publishBulkStatusChanges(events);
|
|
425
|
+
Test.stopTest();
|
|
426
|
+
|
|
427
|
+
for (Database.SaveResult result : results) {
|
|
428
|
+
System.assert(result.isSuccess(), 'Event publish should succeed');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@IsTest
|
|
433
|
+
static void testSubscriberCreatesTasksForFailedOrders() {
|
|
434
|
+
// Create a real Order so the subscriber can validate the ID via SOQL
|
|
435
|
+
Account acc = new Account(Name = 'Test Account');
|
|
436
|
+
insert acc;
|
|
437
|
+
Order ord = new Order(
|
|
438
|
+
AccountId = acc.Id,
|
|
439
|
+
EffectiveDate = Date.today(),
|
|
440
|
+
Status = 'Draft'
|
|
441
|
+
);
|
|
442
|
+
insert ord;
|
|
443
|
+
|
|
444
|
+
List<Order_Status_Event__e> events = new List<Order_Status_Event__e>{
|
|
445
|
+
new Order_Status_Event__e(
|
|
446
|
+
Order_Id__c = ord.Id,
|
|
447
|
+
Status__c = 'Failed',
|
|
448
|
+
Message__c = 'Payment declined'
|
|
449
|
+
),
|
|
450
|
+
new Order_Status_Event__e(
|
|
451
|
+
Order_Id__c = ord.Id,
|
|
452
|
+
Status__c = 'Shipped',
|
|
453
|
+
Message__c = 'Shipped successfully'
|
|
454
|
+
)
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
Test.startTest();
|
|
458
|
+
OrderEventSubscriber.handleEvents(events);
|
|
459
|
+
Test.stopTest();
|
|
460
|
+
|
|
461
|
+
List<Task> tasks = [SELECT Subject, Priority, WhatId FROM Task WHERE Subject LIKE 'Order Failed%'];
|
|
462
|
+
System.assertEquals(1, tasks.size(), 'Should create task only for failed order');
|
|
463
|
+
System.assertEquals('High', tasks[0].Priority);
|
|
464
|
+
System.assertEquals(ord.Id, tasks[0].WhatId, 'Task should be linked to the Order');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Key Principles
|
|
470
|
+
|
|
471
|
+
- Use `PublishAfterCommit` to ensure events are only delivered when the transaction succeeds
|
|
472
|
+
- Use `PublishImmediately` only when events must fire regardless of transaction outcome
|
|
473
|
+
- Set resume checkpoints in event triggers to handle large event volumes without hitting limits
|
|
474
|
+
- Subscribe with replay ID `-1` for new events only, or `-2` to replay all retained events (up to 72 hours)
|
|
475
|
+
- Keep event payloads small: include IDs and status, not full record data
|
|
476
|
+
- CDC events are automatically generated; use them instead of custom events when you only need record change notifications
|
|
477
|
+
|
|
478
|
+
## Common Pitfalls
|
|
479
|
+
|
|
480
|
+
- Publishing events inside a loop instead of collecting and bulk-publishing
|
|
481
|
+
- Forgetting that `PublishAfterCommit` events are lost if the transaction rolls back (this is the intended behavior)
|
|
482
|
+
- Not handling the `onError` callback in empApi, which causes silent subscription failures
|
|
483
|
+
- Exceeding the daily Platform Event allocation (check org limits)
|
|
484
|
+
- Assuming event delivery order is guaranteed across different event types
|
|
485
|
+
- Not calling `unsubscribe` in `disconnectedCallback`, which leaks subscriptions
|
|
486
|
+
|
|
487
|
+
## SCC Skills
|
|
488
|
+
|
|
489
|
+
- `/sf-platform-events-cdc` -- review Platform Event and CDC implementations
|
|
490
|
+
- `/sf-apex-best-practices` -- review publisher and subscriber Apex code
|
|
491
|
+
- `/sf-lwc-development` -- review the empApi subscription component
|
|
492
|
+
- `/sf-governor-limits` -- check event publish limits and bulk compliance
|