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.
Files changed (271) hide show
  1. package/.claude-plugin/plugin.json +44 -0
  2. package/.cursor/agents/deep-researcher.md +142 -0
  3. package/.cursor/agents/doc-updater.md +219 -0
  4. package/.cursor/agents/eval-runner.md +335 -0
  5. package/.cursor/agents/learning-engine.md +210 -0
  6. package/.cursor/agents/loop-operator.md +245 -0
  7. package/.cursor/agents/refactor-cleaner.md +119 -0
  8. package/.cursor/agents/sf-admin-agent.md +127 -0
  9. package/.cursor/agents/sf-agentforce-agent.md +126 -0
  10. package/.cursor/agents/sf-apex-agent.md +117 -0
  11. package/.cursor/agents/sf-architect.md +426 -0
  12. package/.cursor/agents/sf-aura-reviewer.md +369 -0
  13. package/.cursor/agents/sf-bugfix-agent.md +101 -0
  14. package/.cursor/agents/sf-flow-agent.md +155 -0
  15. package/.cursor/agents/sf-integration-agent.md +141 -0
  16. package/.cursor/agents/sf-lwc-agent.md +123 -0
  17. package/.cursor/agents/sf-review-agent.md +357 -0
  18. package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
  19. package/.cursor/hooks/adapter.js +81 -0
  20. package/.cursor/hooks/after-file-edit.js +26 -0
  21. package/.cursor/hooks/after-mcp-execution.js +12 -0
  22. package/.cursor/hooks/after-shell-execution.js +30 -0
  23. package/.cursor/hooks/after-tab-file-edit.js +12 -0
  24. package/.cursor/hooks/before-mcp-execution.js +11 -0
  25. package/.cursor/hooks/before-read-file.js +13 -0
  26. package/.cursor/hooks/before-shell-execution.js +29 -0
  27. package/.cursor/hooks/before-submit-prompt.js +23 -0
  28. package/.cursor/hooks/pre-compact.js +7 -0
  29. package/.cursor/hooks/session-end.js +10 -0
  30. package/.cursor/hooks/session-start.js +10 -0
  31. package/.cursor/hooks/stop.js +18 -0
  32. package/.cursor/hooks/subagent-start.js +10 -0
  33. package/.cursor/hooks/subagent-stop.js +10 -0
  34. package/.cursor/hooks.json +107 -0
  35. package/.cursor/skills/aside/SKILL.md +115 -0
  36. package/.cursor/skills/checkpoint/SKILL.md +50 -0
  37. package/.cursor/skills/configure-scc/SKILL.md +160 -0
  38. package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
  39. package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
  40. package/.cursor/skills/model-route/SKILL.md +81 -0
  41. package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
  42. package/.cursor/skills/refactor-clean/SKILL.md +133 -0
  43. package/.cursor/skills/resume-session/SKILL.md +111 -0
  44. package/.cursor/skills/save-session/SKILL.md +183 -0
  45. package/.cursor/skills/search-first/SKILL.md +140 -0
  46. package/.cursor/skills/security-scan/SKILL.md +142 -0
  47. package/.cursor/skills/sessions/SKILL.md +124 -0
  48. package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
  49. package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
  50. package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
  51. package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
  52. package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
  53. package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
  54. package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
  55. package/.cursor/skills/sf-api-design/SKILL.md +237 -0
  56. package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
  57. package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
  58. package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
  59. package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
  60. package/.cursor/skills/sf-debugging/SKILL.md +362 -0
  61. package/.cursor/skills/sf-deployment/SKILL.md +291 -0
  62. package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
  63. package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
  64. package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
  65. package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
  66. package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
  67. package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
  68. package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
  69. package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
  70. package/.cursor/skills/sf-help/SKILL.md +156 -0
  71. package/.cursor/skills/sf-integration/SKILL.md +479 -0
  72. package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
  73. package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
  74. package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
  75. package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
  76. package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
  77. package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
  78. package/.cursor/skills/sf-security/SKILL.md +330 -0
  79. package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
  80. package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
  81. package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
  82. package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
  83. package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
  84. package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
  85. package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
  86. package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
  87. package/.cursor/skills/strategic-compact/SKILL.md +205 -0
  88. package/.cursor/skills/update-docs/SKILL.md +162 -0
  89. package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
  90. package/.cursor-plugin/plugin.json +26 -0
  91. package/LICENSE +21 -0
  92. package/README.md +522 -0
  93. package/agents/deep-researcher.md +145 -0
  94. package/agents/doc-updater.md +222 -0
  95. package/agents/eval-runner.md +340 -0
  96. package/agents/learning-engine.md +211 -0
  97. package/agents/loop-operator.md +247 -0
  98. package/agents/refactor-cleaner.md +122 -0
  99. package/agents/sf-admin-agent.md +131 -0
  100. package/agents/sf-agentforce-agent.md +132 -0
  101. package/agents/sf-apex-agent.md +124 -0
  102. package/agents/sf-architect.md +435 -0
  103. package/agents/sf-aura-reviewer.md +372 -0
  104. package/agents/sf-bugfix-agent.md +105 -0
  105. package/agents/sf-flow-agent.md +159 -0
  106. package/agents/sf-integration-agent.md +146 -0
  107. package/agents/sf-lwc-agent.md +127 -0
  108. package/agents/sf-review-agent.md +366 -0
  109. package/agents/sf-visualforce-reviewer.md +468 -0
  110. package/assets/logo.svg +18 -0
  111. package/docs/ARCHITECTURE.md +133 -0
  112. package/docs/authoring-guide.md +373 -0
  113. package/docs/hook-development.md +578 -0
  114. package/docs/token-optimization.md +139 -0
  115. package/docs/workflow-examples.md +645 -0
  116. package/examples/agentforce-action/README.md +227 -0
  117. package/examples/apex-trigger-handler/README.md +114 -0
  118. package/examples/devops-pipeline/README.md +325 -0
  119. package/examples/flow-automation/README.md +188 -0
  120. package/examples/integration-pattern/README.md +416 -0
  121. package/examples/lwc-component/README.md +180 -0
  122. package/examples/platform-events/README.md +492 -0
  123. package/examples/scratch-org-setup/README.md +138 -0
  124. package/examples/security-audit/README.md +244 -0
  125. package/examples/visualforce-migration/README.md +314 -0
  126. package/hooks/hooks.json +338 -0
  127. package/hooks/memory-persistence/README.md +73 -0
  128. package/manifests/install-modules.json +217 -0
  129. package/manifests/install-profiles.json +17 -0
  130. package/mcp-configs/mcp-servers.json +19 -0
  131. package/package.json +89 -0
  132. package/schemas/hooks.schema.json +123 -0
  133. package/schemas/install-modules.schema.json +76 -0
  134. package/schemas/install-profiles.schema.json +28 -0
  135. package/schemas/install-state.schema.json +73 -0
  136. package/schemas/package-manager.schema.json +18 -0
  137. package/schemas/plugin.schema.json +112 -0
  138. package/schemas/scc-install-config.schema.json +29 -0
  139. package/schemas/state-store.schema.json +111 -0
  140. package/scripts/cli/install-apply.js +170 -0
  141. package/scripts/cli/uninstall.js +193 -0
  142. package/scripts/hooks/check-console-log.js +101 -0
  143. package/scripts/hooks/check-hook-enabled.js +17 -0
  144. package/scripts/hooks/check-platform-docs-age.js +48 -0
  145. package/scripts/hooks/cost-tracker.js +78 -0
  146. package/scripts/hooks/doc-file-warning.js +63 -0
  147. package/scripts/hooks/evaluate-session.js +98 -0
  148. package/scripts/hooks/governor-check.js +220 -0
  149. package/scripts/hooks/learning-observe.sh +206 -0
  150. package/scripts/hooks/mcp-health-check.js +588 -0
  151. package/scripts/hooks/post-bash-build-complete.js +34 -0
  152. package/scripts/hooks/post-bash-pr-created.js +43 -0
  153. package/scripts/hooks/post-edit-console-warn.js +61 -0
  154. package/scripts/hooks/post-edit-format.js +79 -0
  155. package/scripts/hooks/post-edit-typecheck.js +98 -0
  156. package/scripts/hooks/post-write.js +168 -0
  157. package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
  158. package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
  159. package/scripts/hooks/pre-compact.js +51 -0
  160. package/scripts/hooks/pre-tool-use.js +163 -0
  161. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  162. package/scripts/hooks/quality-gate.js +251 -0
  163. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  164. package/scripts/hooks/run-with-flags.js +135 -0
  165. package/scripts/hooks/session-end-marker.js +29 -0
  166. package/scripts/hooks/session-end.js +311 -0
  167. package/scripts/hooks/session-start.js +202 -0
  168. package/scripts/hooks/sfdx-scanner-check.js +142 -0
  169. package/scripts/hooks/sfdx-validate.js +119 -0
  170. package/scripts/hooks/stop-hook.js +170 -0
  171. package/scripts/hooks/suggest-compact.js +67 -0
  172. package/scripts/lib/agent-adapter.js +82 -0
  173. package/scripts/lib/apex-analysis.js +194 -0
  174. package/scripts/lib/hook-flags.js +74 -0
  175. package/scripts/lib/install-config.js +73 -0
  176. package/scripts/lib/install-executor.js +363 -0
  177. package/scripts/lib/install-state.js +121 -0
  178. package/scripts/lib/orchestration-session.js +299 -0
  179. package/scripts/lib/package-manager.js +124 -0
  180. package/scripts/lib/project-detect.js +228 -0
  181. package/scripts/lib/schema-validator.js +190 -0
  182. package/scripts/lib/skill-adapter.js +100 -0
  183. package/scripts/lib/state-store.js +376 -0
  184. package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
  185. package/scripts/lib/utils.js +313 -0
  186. package/scripts/scc.js +164 -0
  187. package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
  188. package/skills/_reference/APEX_CURSOR.md +159 -0
  189. package/skills/_reference/API_VERSIONS.md +78 -0
  190. package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
  191. package/skills/_reference/ASYNC_PATTERNS.md +163 -0
  192. package/skills/_reference/AURA_COMPONENTS.md +146 -0
  193. package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
  194. package/skills/_reference/DATA_MODELING.md +124 -0
  195. package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
  196. package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
  197. package/skills/_reference/DEPRECATIONS.md +79 -0
  198. package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
  199. package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
  200. package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
  201. package/skills/_reference/FLOW_PATTERNS.md +113 -0
  202. package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
  203. package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
  204. package/skills/_reference/LWC_PATTERNS.md +79 -0
  205. package/skills/_reference/METADATA_TYPES.md +115 -0
  206. package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
  207. package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
  208. package/skills/_reference/PLATFORM_EVENTS.md +121 -0
  209. package/skills/_reference/REPORTING_API.md +143 -0
  210. package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
  211. package/skills/_reference/SECURITY_PATTERNS.md +127 -0
  212. package/skills/_reference/SHARING_MODEL.md +120 -0
  213. package/skills/_reference/SOQL_PATTERNS.md +119 -0
  214. package/skills/_reference/TESTING_STANDARDS.md +96 -0
  215. package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
  216. package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
  217. package/skills/aside/SKILL.md +118 -0
  218. package/skills/checkpoint/SKILL.md +53 -0
  219. package/skills/configure-scc/SKILL.md +163 -0
  220. package/skills/continuous-agent-loop/SKILL.md +264 -0
  221. package/skills/mcp-server-patterns/SKILL.md +146 -0
  222. package/skills/model-route/SKILL.md +84 -0
  223. package/skills/prompt-optimizer/SKILL.md +369 -0
  224. package/skills/refactor-clean/SKILL.md +136 -0
  225. package/skills/resume-session/SKILL.md +114 -0
  226. package/skills/save-session/SKILL.md +186 -0
  227. package/skills/search-first/SKILL.md +144 -0
  228. package/skills/security-scan/SKILL.md +146 -0
  229. package/skills/sessions/SKILL.md +127 -0
  230. package/skills/sf-agentforce-development/SKILL.md +450 -0
  231. package/skills/sf-apex-async-patterns/SKILL.md +326 -0
  232. package/skills/sf-apex-best-practices/SKILL.md +425 -0
  233. package/skills/sf-apex-constraints/SKILL.md +81 -0
  234. package/skills/sf-apex-cursor/SKILL.md +338 -0
  235. package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
  236. package/skills/sf-apex-testing/SKILL.md +409 -0
  237. package/skills/sf-api-design/SKILL.md +238 -0
  238. package/skills/sf-approval-processes/SKILL.md +315 -0
  239. package/skills/sf-aura-development/SKILL.md +263 -0
  240. package/skills/sf-build-fix/SKILL.md +121 -0
  241. package/skills/sf-data-modeling/SKILL.md +278 -0
  242. package/skills/sf-debugging/SKILL.md +363 -0
  243. package/skills/sf-deployment/SKILL.md +295 -0
  244. package/skills/sf-deployment-constraints/SKILL.md +155 -0
  245. package/skills/sf-devops-ci-cd/SKILL.md +325 -0
  246. package/skills/sf-docs-lookup/SKILL.md +103 -0
  247. package/skills/sf-e2e-testing/SKILL.md +324 -0
  248. package/skills/sf-experience-cloud/SKILL.md +249 -0
  249. package/skills/sf-flow-development/SKILL.md +377 -0
  250. package/skills/sf-governor-limits/SKILL.md +323 -0
  251. package/skills/sf-harness-audit/SKILL.md +142 -0
  252. package/skills/sf-help/SKILL.md +159 -0
  253. package/skills/sf-integration/SKILL.md +483 -0
  254. package/skills/sf-lwc-constraints/SKILL.md +130 -0
  255. package/skills/sf-lwc-development/SKILL.md +303 -0
  256. package/skills/sf-lwc-testing/SKILL.md +388 -0
  257. package/skills/sf-metadata-management/SKILL.md +288 -0
  258. package/skills/sf-platform-events-cdc/SKILL.md +375 -0
  259. package/skills/sf-quickstart/SKILL.md +173 -0
  260. package/skills/sf-security/SKILL.md +334 -0
  261. package/skills/sf-security-constraints/SKILL.md +127 -0
  262. package/skills/sf-soql-constraints/SKILL.md +131 -0
  263. package/skills/sf-soql-optimization/SKILL.md +354 -0
  264. package/skills/sf-tdd-workflow/SKILL.md +336 -0
  265. package/skills/sf-testing-constraints/SKILL.md +200 -0
  266. package/skills/sf-trigger-constraints/SKILL.md +90 -0
  267. package/skills/sf-trigger-frameworks/SKILL.md +347 -0
  268. package/skills/sf-visualforce-development/SKILL.md +260 -0
  269. package/skills/strategic-compact/SKILL.md +208 -0
  270. package/skills/update-docs/SKILL.md +165 -0
  271. package/skills/update-platform-docs/SKILL.md +90 -0
@@ -0,0 +1,338 @@
1
+ ---
2
+ name: sf-apex-cursor
3
+ description: "Apex Cursor API for paginating large SOQL results (up to 50M records) — cursor navigation, Queueable chaining, LWC pagination. Use when paginating queries or migrating from OFFSET. Do NOT use for small result sets."
4
+ origin: SCC
5
+ user-invocable: false
6
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
7
+ ---
8
+
9
+ # Apex Cursor
10
+
11
+ The `Cursor` class (GA Spring '26) enables efficient pagination through up to 50 million SOQL rows without the 2,000-row OFFSET limit. Use it for large dataset processing that previously required chunked OFFSET patterns or raw Batch Apex.
12
+
13
+ Reference: @../_reference/APEX_CURSOR.md
14
+
15
+ ---
16
+
17
+ ## When to Use
18
+
19
+ - When implementing paginated queries over large datasets using the Apex Cursor API
20
+ - When OFFSET-based pagination hits governor limits or performance issues on large result sets
21
+ - When building `@AuraEnabled` methods with server-side cursor pagination for LWC components
22
+ - When migrating legacy OFFSET queries to cursor-based iteration for scalability beyond 2,000 rows
23
+
24
+ ---
25
+
26
+ ## Cursor vs. OFFSET vs. Batch Apex
27
+
28
+ | Approach | Max Records | Heap Impact | Best For |
29
+ |----------|------------|-------------|----------|
30
+ | SOQL `OFFSET` | 2,000 | Full result set in heap | Small UI pagination |
31
+ | Batch Apex | Unlimited | Per-execute governor reset | Background mass processing |
32
+ | `Cursor` class | 50,000,000 | Per-page only | Large paginated reports, async chaining, LWC infinite scroll |
33
+
34
+ ### Performance Comparison
35
+
36
+ | Record Count | Best Approach | Why |
37
+ |-------------|--------------|-----|
38
+ | < 200 | Standard SOQL with `LIMIT` | Simple, no overhead |
39
+ | 200 - 2,000 | `OFFSET` pagination | Adequate performance, simpler code |
40
+ | 2,000 - 50,000 | `Cursor` | OFFSET degrades above 2K; Cursor maintains constant performance |
41
+ | 50,000+ | `Cursor` + Queueable chaining | Single cursor handles up to 50M records |
42
+ | Batch processing | `Database.QueryLocator` | Full governor reset per execute chunk |
43
+
44
+ **Key insight:** OFFSET forces the database to skip N rows on every request. At 10,000 OFFSET, the DB scans and discards 10K rows. Cursor maintains a server-side pointer with no scanning overhead regardless of position.
45
+
46
+ ---
47
+
48
+ ## Cursor Class API
49
+
50
+ ```apex
51
+ // Open a cursor — returns a server-side pointer, not the data
52
+ Database.Cursor cursor = Database.getCursor('SELECT Id, Name FROM Account ORDER BY Id');
53
+
54
+ // Fetch a page of records starting at offset
55
+ List<SObject> page = cursor.fetch(offset, pageSize);
56
+
57
+ // Total number of records the cursor can return
58
+ Integer total = cursor.getNumRecords();
59
+
60
+ // Serialize the cursor for use across transactions (Queueable chaining)
61
+ String cursorId = cursor.getId();
62
+
63
+ // Re-open a serialized cursor in a new transaction
64
+ Database.Cursor resumed = Database.getCursor(cursorId);
65
+
66
+ // Always close when done to release server-side resources
67
+ cursor.close();
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Basic Cursor Pagination
73
+
74
+ Process data page-by-page without loading everything into heap. Do not accumulate all rows in memory.
75
+
76
+ ```apex
77
+ public class LargeAccountAuditor {
78
+
79
+ public static AuditSummary auditAccounts() {
80
+ Database.Cursor cursor = Database.getCursor(
81
+ 'SELECT Id, Name, AnnualRevenue, Industry FROM Account ORDER BY Name'
82
+ );
83
+
84
+ Integer pageSize = 2000;
85
+ Integer offset = 0;
86
+ Decimal totalRevenue = 0;
87
+ Integer totalCount = 0;
88
+ List<Audit_Log__c> logsToInsert = new List<Audit_Log__c>();
89
+
90
+ try {
91
+ while (offset < cursor.getNumRecords()) {
92
+ List<Account> page = cursor.fetch(offset, pageSize);
93
+
94
+ for (Account acc : page) {
95
+ totalRevenue += acc.AnnualRevenue != null ? acc.AnnualRevenue : 0;
96
+ totalCount++;
97
+
98
+ if (acc.AnnualRevenue == null || acc.AnnualRevenue == 0) {
99
+ logsToInsert.add(new Audit_Log__c(
100
+ Record_Id__c = acc.Id,
101
+ Finding__c = 'Missing AnnualRevenue'
102
+ ));
103
+ }
104
+ }
105
+ // page goes out of scope — GC-eligible, heap stays flat
106
+
107
+ if (logsToInsert.size() >= 5000) {
108
+ insert logsToInsert;
109
+ logsToInsert.clear();
110
+ }
111
+
112
+ offset += pageSize;
113
+ }
114
+
115
+ if (!logsToInsert.isEmpty()) insert logsToInsert;
116
+ } finally {
117
+ cursor.close();
118
+ }
119
+
120
+ return new AuditSummary(totalCount, totalRevenue);
121
+ }
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Async Cursor Chaining with Queueable
128
+
129
+ Serialize a Cursor by ID and pass it across Queueable jobs to process 50M records across chained async transactions.
130
+
131
+ ```apex
132
+ public class LargeLeadProcessorQueueable implements Queueable {
133
+
134
+ private String cursorId;
135
+ private Integer offset;
136
+ private static final Integer PAGE_SIZE = 2000;
137
+
138
+ // First call — no cursor yet
139
+ public LargeLeadProcessorQueueable() {
140
+ Database.Cursor cursor = Database.getCursor(
141
+ 'SELECT Id, Status, LeadSource FROM Lead WHERE IsConverted = false ORDER BY Id'
142
+ );
143
+ this.cursorId = cursor.getId();
144
+ this.offset = 0;
145
+ cursor.close(); // Close handle; cursor remains alive on server
146
+ }
147
+
148
+ // Subsequent calls — resume from cursor
149
+ public LargeLeadProcessorQueueable(String cursorId, Integer offset) {
150
+ this.cursorId = cursorId;
151
+ this.offset = offset;
152
+ }
153
+
154
+ public void execute(QueueableContext ctx) {
155
+ Database.Cursor cursor = Database.getCursor(this.cursorId);
156
+
157
+ if (this.offset >= cursor.getNumRecords()) {
158
+ cursor.close();
159
+ return;
160
+ }
161
+
162
+ List<Lead> page = cursor.fetch(this.offset, PAGE_SIZE);
163
+ processLeads(page);
164
+
165
+ Integer nextOffset = this.offset + page.size();
166
+
167
+ if (nextOffset < cursor.getNumRecords()) {
168
+ cursor.close();
169
+ System.enqueueJob(new LargeLeadProcessorQueueable(this.cursorId, nextOffset));
170
+ } else {
171
+ cursor.close();
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ ---
178
+
179
+ ## PaginationCursor for LWC / @AuraEnabled
180
+
181
+ For user-facing pagination (LWC infinite scroll, Screen Flows), use `Database.PaginationCursor`. It is `@AuraEnabled` compatible.
182
+
183
+ ```apex
184
+ public with sharing class AccountPaginationController {
185
+
186
+ private static final Integer DEFAULT_PAGE_SIZE = 20;
187
+
188
+ @AuraEnabled(cacheable=false)
189
+ public static PageResult getAccounts(String cursorId, Integer pageSize) {
190
+ if (pageSize == null || pageSize <= 0) pageSize = DEFAULT_PAGE_SIZE;
191
+
192
+ Database.PaginationCursor cursor;
193
+ if (String.isBlank(cursorId)) {
194
+ cursor = Database.getPaginationCursor(
195
+ 'SELECT Id, Name, Industry, AnnualRevenue FROM Account ORDER BY Name'
196
+ );
197
+ } else {
198
+ cursor = Database.getPaginationCursor(cursorId);
199
+ }
200
+
201
+ List<Account> page = cursor.fetch(pageSize);
202
+
203
+ PageResult result = new PageResult();
204
+ result.records = page;
205
+ result.cursorId = cursor.getId();
206
+ result.hasMore = cursor.hasMore();
207
+ result.totalCount = cursor.getNumRecords();
208
+ return result;
209
+ }
210
+
211
+ public class PageResult {
212
+ @AuraEnabled public List<Account> records;
213
+ @AuraEnabled public String cursorId;
214
+ @AuraEnabled public Boolean hasMore;
215
+ @AuraEnabled public Integer totalCount;
216
+ }
217
+ }
218
+ ```
219
+
220
+ ### LWC Integration
221
+
222
+ ```javascript
223
+ import { LightningElement } from 'lwc';
224
+ import getAccounts from '@salesforce/apex/AccountPaginationController.getAccounts';
225
+
226
+ export default class AccountInfiniteList extends LightningElement {
227
+ accounts = [];
228
+ cursorId = null;
229
+ hasMore = true;
230
+ isLoading = false;
231
+
232
+ connectedCallback() { this.loadPage(); }
233
+
234
+ async loadPage() {
235
+ if (this.isLoading || !this.hasMore) return;
236
+ this.isLoading = true;
237
+ try {
238
+ const result = await getAccounts({ cursorId: this.cursorId, pageSize: 20 });
239
+ this.accounts = [...this.accounts, ...result.records];
240
+ this.cursorId = result.cursorId;
241
+ this.hasMore = result.hasMore;
242
+ } catch (error) {
243
+ console.error('Pagination error:', error);
244
+ } finally {
245
+ this.isLoading = false;
246
+ }
247
+ }
248
+
249
+ handleLoadMore() { this.loadPage(); }
250
+ }
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Cursor Limits
256
+
257
+ | Constraint | Detail |
258
+ |-----------|--------|
259
+ | Max records per cursor | 50,000,000 |
260
+ | Cursor lifetime (sync) | 10 minutes |
261
+ | Cursor lifetime (async) | 60 minutes |
262
+ | `fetch()` max page size | 2,000 rows per call |
263
+ | Max open cursors | 10 per transaction |
264
+ | `PaginationCursor` @AuraEnabled | Fully supported |
265
+
266
+ ### Error Handling
267
+
268
+ Always close cursors in a `try/finally` block.
269
+
270
+ ```apex
271
+ Database.Cursor cursor = Database.getCursor('SELECT Id FROM Lead');
272
+ try {
273
+ List<Lead> leads = cursor.fetch(0, 100);
274
+ processLeads(leads);
275
+ } catch (System.CursorException e) {
276
+ // Cursor expired or already closed
277
+ System.debug(LoggingLevel.WARN, 'Cursor error: ' + e.getMessage());
278
+ throw;
279
+ } finally {
280
+ try { cursor.close(); } catch (Exception ignored) {}
281
+ }
282
+ ```
283
+
284
+ | Error | Cause | Fix |
285
+ |-------|-------|-----|
286
+ | `Cursor has been closed` | `fetch()` after `close()` | Check cursor state before fetching |
287
+ | `Cursor has expired` | > 10 min sync / 60 min async | Process faster or use Queueable chaining |
288
+ | `Maximum cursors exceeded` | > 10 open cursors | Close cursors in `finally` blocks |
289
+ | `Non-selective query` | WHERE clause not indexed | Add custom index or narrow query |
290
+
291
+ ---
292
+
293
+ ## Testing Cursor Code
294
+
295
+ ```apex
296
+ @IsTest
297
+ private class CursorPaginationTest {
298
+
299
+ @TestSetup
300
+ static void makeData() {
301
+ List<Account> accounts = new List<Account>();
302
+ for (Integer i = 0; i < 500; i++) {
303
+ accounts.add(new Account(Name = 'Cursor Test ' + String.valueOf(i).leftPad(3, '0')));
304
+ }
305
+ insert accounts;
306
+ }
307
+
308
+ @IsTest
309
+ static void shouldPaginateThroughAllRecords() {
310
+ Test.startTest();
311
+ Database.Cursor cursor = Database.getCursor(
312
+ 'SELECT Id, Name FROM Account WHERE Name LIKE \'Cursor Test%\' ORDER BY Name'
313
+ );
314
+ Integer totalFetched = 0;
315
+ try {
316
+ while (totalFetched < cursor.getNumRecords()) {
317
+ List<Account> page = (List<Account>) cursor.fetch(totalFetched, 200);
318
+ totalFetched += page.size();
319
+ }
320
+ } finally {
321
+ cursor.close();
322
+ }
323
+ Test.stopTest();
324
+ System.assertEquals(500, totalFetched, 'Should fetch all 500 records');
325
+ }
326
+ }
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Related
332
+
333
+ - **Skills**: `sf-apex-async-patterns` — For Queueable chaining patterns
334
+
335
+ ### Guardrails
336
+
337
+ - `sf-apex-constraints` — Governs SOQL and DML usage in cursor-processing code
338
+ - `sf-soql-constraints` — Governs query structure and selectivity requirements
@@ -0,0 +1,348 @@
1
+ ---
2
+ name: sf-apex-enterprise-patterns
3
+ description: >-
4
+ Use when implementing Salesforce Apex Enterprise Patterns (FFLIB) — Selector,
5
+ Domain, Service, Unit of Work layers. Do NOT use for simple orgs or constraints.
6
+ origin: SCC
7
+ user-invocable: false
8
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
9
+ ---
10
+
11
+ # Apex Enterprise Patterns
12
+
13
+ Implementation guidance for Apex Enterprise Patterns (AEP / FFLIB). Covers the four-layer architecture, pragmatic adoption, and when NOT to use them. Constraint rules live in `sf-apex-constraints`.
14
+
15
+ Reference: @../_reference/ENTERPRISE_PATTERNS.md
16
+
17
+ ---
18
+
19
+ ## When to Use
20
+
21
+ - When building Apex applications that will scale beyond 5 developers or 50 custom classes
22
+ - When trigger logic is becoming complex and duplicated across multiple contexts
23
+ - When SOQL queries are scattered throughout classes instead of centralized
24
+ - When implementing FFLIB (Andy Fawcett) patterns in a Salesforce project
25
+ - When separating business logic from trigger context to improve testability
26
+ - When a service method needs to coordinate inserts, updates, and deletes atomically
27
+
28
+ ## When NOT to Use
29
+
30
+ - **Simple automations**: A before-insert trigger that sets a default status
31
+ - **One-off scripts**: Data migration or fix scripts
32
+ - **Small orgs** (< 5 developers, < 50 custom classes)
33
+ - **Read-only visualizations**: A Selector is often sufficient
34
+
35
+ The rule: introduce a layer when the absence of that layer is causing a real problem.
36
+
37
+ ---
38
+
39
+ ## Architecture Overview
40
+
41
+ ```
42
+ Trigger / Controller / API
43
+ |
44
+ Service Layer <- Transaction boundary, orchestration
45
+ |
46
+ Domain Layer <- Business rules on record collections
47
+ |
48
+ Selector Layer <- All SOQL queries
49
+ |
50
+ Unit of Work <- All DML (atomic commit)
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Selector Layer
56
+
57
+ Selectors own all SOQL queries for an object. No SOQL appears outside a Selector.
58
+
59
+ **Naming:** `{ObjectNamePlural}Selector` — e.g., `AccountsSelector`, `OpportunitiesSelector`
60
+
61
+ ### Without FFLIB
62
+
63
+ ```apex
64
+ public with sharing class AccountsSelector {
65
+
66
+ @TestVisible
67
+ private static AccountsSelector instance;
68
+
69
+ public static AccountsSelector newInstance() {
70
+ if (instance == null) instance = new AccountsSelector();
71
+ return instance;
72
+ }
73
+
74
+ public List<Account> selectById(Set<Id> accountIds) {
75
+ return [
76
+ SELECT Id, Name, Type, OwnerId, AnnualRevenue,
77
+ Customer_Tier__c, CreditLimit__c
78
+ FROM Account WHERE Id IN :accountIds
79
+ WITH USER_MODE ORDER BY Name
80
+ ];
81
+ }
82
+
83
+ public List<Account> selectWithOpenOpportunitiesById(Set<Id> accountIds) {
84
+ return [
85
+ SELECT Id, Name, AnnualRevenue, Customer_Tier__c,
86
+ (SELECT Id, Name, Amount, CloseDate, StageName
87
+ FROM Opportunities WHERE IsClosed = false
88
+ ORDER BY CloseDate ASC)
89
+ FROM Account WHERE Id IN :accountIds WITH USER_MODE
90
+ ];
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### With FFLIB
96
+
97
+ ```apex
98
+ public with sharing class AccountsSelector extends fflib_SObjectSelector {
99
+
100
+ public static AccountsSelector newInstance() {
101
+ return (AccountsSelector) Application.Selector.newInstance(Account.SObjectType);
102
+ }
103
+
104
+ public Schema.SObjectType getSObjectType() { return Account.SObjectType; }
105
+
106
+ public List<Schema.SObjectField> getSObjectFieldList() {
107
+ return new List<Schema.SObjectField>{
108
+ Account.Id, Account.Name, Account.Type,
109
+ Account.OwnerId, Account.AnnualRevenue
110
+ };
111
+ }
112
+
113
+ public List<Account> selectById(Set<Id> accountIds) {
114
+ return (List<Account>) selectSObjectsById(accountIds);
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Domain Layer
122
+
123
+ Encapsulates all business logic for a collection of records of the same type. Replaces trigger logic.
124
+
125
+ **Naming:** `{ObjectNamePlural}` — e.g., `Accounts`, `Opportunities`
126
+
127
+ ```apex
128
+ public with sharing class Accounts {
129
+
130
+ private final List<Account> records;
131
+ private final Map<Id, Account> existingRecords;
132
+
133
+ public static Accounts newInstance(List<Account> records) {
134
+ return new Accounts(records, null);
135
+ }
136
+
137
+ public static Accounts newInstance(List<Account> records, Map<Id, Account> existing) {
138
+ return new Accounts(records, existing);
139
+ }
140
+
141
+ private Accounts(List<Account> records, Map<Id, Account> existingRecords) {
142
+ this.records = records;
143
+ this.existingRecords = existingRecords;
144
+ }
145
+
146
+ public void onBeforeInsert() {
147
+ setDefaultCustomerTier();
148
+ validateRequiredFields();
149
+ }
150
+
151
+ public void onBeforeUpdate() {
152
+ validateRequiredFields();
153
+ preventDowngradingPremiumTier();
154
+ }
155
+
156
+ public void setDefaultCustomerTier() {
157
+ for (Account acc : records) {
158
+ if (String.isBlank(acc.Customer_Tier__c)) acc.Customer_Tier__c = 'Standard';
159
+ }
160
+ }
161
+
162
+ public void validateRequiredFields() {
163
+ for (Account acc : records) {
164
+ if (acc.Type == 'Customer' && String.isBlank(acc.Industry)) {
165
+ acc.Industry.addError('Industry is required for Customer account type.');
166
+ }
167
+ }
168
+ }
169
+
170
+ public void preventDowngradingPremiumTier() {
171
+ for (Account acc : records) {
172
+ Account existing = existingRecords?.get(acc.Id);
173
+ if (existing == null) continue;
174
+ if (existing.Customer_Tier__c == 'Premium'
175
+ && acc.Customer_Tier__c != 'Premium') {
176
+ acc.Customer_Tier__c.addError(
177
+ 'Premium tier downgrade requires approval.'
178
+ );
179
+ }
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Trigger Using Domain Layer
186
+
187
+ ```apex
188
+ trigger AccountTrigger on Account (
189
+ before insert, before update, after insert, after update
190
+ ) {
191
+ if (Trigger.isBefore && Trigger.isInsert) {
192
+ Accounts.newInstance(Trigger.new).onBeforeInsert();
193
+ } else if (Trigger.isBefore && Trigger.isUpdate) {
194
+ Accounts.newInstance(Trigger.new, Trigger.oldMap).onBeforeUpdate();
195
+ }
196
+ }
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Service Layer
202
+
203
+ Orchestrates business processes that span multiple objects or require a full transaction boundary.
204
+
205
+ **Naming:** `{ObjectNamePlural}Service` — e.g., `AccountsService`
206
+
207
+ **Rules:**
208
+
209
+ 1. Static methods only — services are stateless
210
+ 2. No SOQL — delegate to Selectors
211
+ 3. No direct DML — use Unit of Work
212
+ 4. Owns the transaction boundary
213
+ 5. Calls Domain methods for record-level rules
214
+
215
+ ```apex
216
+ public with sharing class AccountsService {
217
+
218
+ public static void upgradeToPremium(Set<Id> accountIds) {
219
+ List<Account> accounts = AccountsSelector.newInstance()
220
+ .selectWithOpenOpportunitiesById(accountIds);
221
+
222
+ if (accounts.isEmpty()) {
223
+ throw new UpgradeException('No accounts found for IDs: ' + accountIds);
224
+ }
225
+
226
+ // Validate
227
+ List<String> errors = validateForUpgrade(accounts);
228
+ if (!errors.isEmpty()) {
229
+ throw new UpgradeException(String.join(errors, '\n'));
230
+ }
231
+
232
+ // Build Unit of Work
233
+ fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
234
+ for (Account acc : accounts) {
235
+ acc.Customer_Tier__c = 'Premium';
236
+ acc.CreditLimit__c = 100000.00;
237
+ uow.registerDirty(acc);
238
+
239
+ uow.registerNew(new Opportunity(
240
+ Name = acc.Name + ' - Premium Welcome',
241
+ AccountId = acc.Id,
242
+ StageName = 'Qualification',
243
+ CloseDate = Date.today().addDays(30)
244
+ ));
245
+ }
246
+
247
+ uow.commitWork(); // One atomic DML transaction
248
+ }
249
+
250
+ public class UpgradeException extends Exception {}
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Unit of Work
257
+
258
+ Accumulates all DML operations and commits them in a single, ordered, atomic transaction.
259
+
260
+ ### Lightweight Implementation (No FFLIB)
261
+
262
+ ```apex
263
+ public class SimpleUnitOfWork {
264
+
265
+ private List<SObject> toInsert = new List<SObject>();
266
+ private List<SObject> toUpdate = new List<SObject>();
267
+ private List<SObject> toDelete = new List<SObject>();
268
+
269
+ public void registerNew(SObject record) { toInsert.add(record); }
270
+ public void registerDirty(SObject record) { toUpdate.add(record); }
271
+ public void registerDeleted(SObject record) { toDelete.add(record); }
272
+
273
+ public void commitWork() {
274
+ Savepoint sp = Database.setSavepoint();
275
+ try {
276
+ if (!toInsert.isEmpty()) insert toInsert;
277
+ if (!toUpdate.isEmpty()) update toUpdate;
278
+ if (!toDelete.isEmpty()) delete toDelete;
279
+ } catch (Exception e) {
280
+ Database.rollback(sp);
281
+ throw e;
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
287
+ ### FFLIB Application Factory
288
+
289
+ ```apex
290
+ public class Application {
291
+ public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
292
+ new fflib_Application.UnitOfWorkFactory(
293
+ new List<SObjectType>{
294
+ Account.SObjectType,
295
+ Contact.SObjectType,
296
+ Opportunity.SObjectType
297
+ }
298
+ );
299
+
300
+ public static final fflib_Application.SelectorFactory Selector =
301
+ new fflib_Application.SelectorFactory(
302
+ new Map<SObjectType, Type>{
303
+ Account.SObjectType => AccountsSelector.class,
304
+ Opportunity.SObjectType => OpportunitiesSelector.class
305
+ }
306
+ );
307
+ }
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Pragmatic Adoption Path
313
+
314
+ ### Phase 1: Selector + Service (Most Immediate Value)
315
+
316
+ Centralize SOQL into Selectors, business processes into Services. No FFLIB dependency needed.
317
+
318
+ ### Phase 2: Add Domain Layer for Trigger Logic
319
+
320
+ When trigger logic grows beyond simple field defaults, introduce the Domain layer.
321
+
322
+ ### Phase 3: Add Unit of Work for Complex Transactions
323
+
324
+ When a service needs to insert/update multiple related objects, introduce UoW for atomicity.
325
+
326
+ ---
327
+
328
+ ## FFLIB Installation
329
+
330
+ ```bash
331
+ # Clone and deploy FFLIB
332
+ git clone https://github.com/apex-enterprise-patterns/fflib-apex-common.git
333
+ git clone https://github.com/apex-enterprise-patterns/fflib-apex-mocks.git
334
+ sf project deploy start --source-dir fflib-apex-common/sfdx-source --target-org my-org
335
+ sf project deploy start --source-dir fflib-apex-mocks/sfdx-source --target-org my-org
336
+ ```
337
+
338
+ > FFLIB is typically deployed as unmanaged source code directly from the cloned repositories, not as a versioned managed package.
339
+
340
+ ---
341
+
342
+ ## Related
343
+
344
+ - **Agents**: `sf-review-agent`, `sf-architect` — For interactive guidance
345
+
346
+ ### Guardrails
347
+
348
+ - `sf-apex-constraints` — Governs all Apex code including enterprise pattern implementations