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,336 @@
1
+ ---
2
+ name: sf-apex-cursor
3
+ description: >-
4
+ 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.
5
+ ---
6
+
7
+ # Apex Cursor
8
+
9
+ 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.
10
+
11
+ Reference: @../_reference/APEX_CURSOR.md
12
+
13
+ ---
14
+
15
+ ## When to Use
16
+
17
+ - When implementing paginated queries over large datasets using the Apex Cursor API
18
+ - When OFFSET-based pagination hits governor limits or performance issues on large result sets
19
+ - When building `@AuraEnabled` methods with server-side cursor pagination for LWC components
20
+ - When migrating legacy OFFSET queries to cursor-based iteration for scalability beyond 2,000 rows
21
+
22
+ ---
23
+
24
+ ## Cursor vs. OFFSET vs. Batch Apex
25
+
26
+ | Approach | Max Records | Heap Impact | Best For |
27
+ |----------|------------|-------------|----------|
28
+ | SOQL `OFFSET` | 2,000 | Full result set in heap | Small UI pagination |
29
+ | Batch Apex | Unlimited | Per-execute governor reset | Background mass processing |
30
+ | `Cursor` class | 50,000,000 | Per-page only | Large paginated reports, async chaining, LWC infinite scroll |
31
+
32
+ ### Performance Comparison
33
+
34
+ | Record Count | Best Approach | Why |
35
+ |-------------|--------------|-----|
36
+ | < 200 | Standard SOQL with `LIMIT` | Simple, no overhead |
37
+ | 200 - 2,000 | `OFFSET` pagination | Adequate performance, simpler code |
38
+ | 2,000 - 50,000 | `Cursor` | OFFSET degrades above 2K; Cursor maintains constant performance |
39
+ | 50,000+ | `Cursor` + Queueable chaining | Single cursor handles up to 50M records |
40
+ | Batch processing | `Database.QueryLocator` | Full governor reset per execute chunk |
41
+
42
+ **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.
43
+
44
+ ---
45
+
46
+ ## Cursor Class API
47
+
48
+ ```apex
49
+ // Open a cursor — returns a server-side pointer, not the data
50
+ Database.Cursor cursor = Database.getCursor('SELECT Id, Name FROM Account ORDER BY Id');
51
+
52
+ // Fetch a page of records starting at offset
53
+ List<SObject> page = cursor.fetch(offset, pageSize);
54
+
55
+ // Total number of records the cursor can return
56
+ Integer total = cursor.getNumRecords();
57
+
58
+ // Serialize the cursor for use across transactions (Queueable chaining)
59
+ String cursorId = cursor.getId();
60
+
61
+ // Re-open a serialized cursor in a new transaction
62
+ Database.Cursor resumed = Database.getCursor(cursorId);
63
+
64
+ // Always close when done to release server-side resources
65
+ cursor.close();
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Basic Cursor Pagination
71
+
72
+ Process data page-by-page without loading everything into heap. Do not accumulate all rows in memory.
73
+
74
+ ```apex
75
+ public class LargeAccountAuditor {
76
+
77
+ public static AuditSummary auditAccounts() {
78
+ Database.Cursor cursor = Database.getCursor(
79
+ 'SELECT Id, Name, AnnualRevenue, Industry FROM Account ORDER BY Name'
80
+ );
81
+
82
+ Integer pageSize = 2000;
83
+ Integer offset = 0;
84
+ Decimal totalRevenue = 0;
85
+ Integer totalCount = 0;
86
+ List<Audit_Log__c> logsToInsert = new List<Audit_Log__c>();
87
+
88
+ try {
89
+ while (offset < cursor.getNumRecords()) {
90
+ List<Account> page = cursor.fetch(offset, pageSize);
91
+
92
+ for (Account acc : page) {
93
+ totalRevenue += acc.AnnualRevenue != null ? acc.AnnualRevenue : 0;
94
+ totalCount++;
95
+
96
+ if (acc.AnnualRevenue == null || acc.AnnualRevenue == 0) {
97
+ logsToInsert.add(new Audit_Log__c(
98
+ Record_Id__c = acc.Id,
99
+ Finding__c = 'Missing AnnualRevenue'
100
+ ));
101
+ }
102
+ }
103
+ // page goes out of scope — GC-eligible, heap stays flat
104
+
105
+ if (logsToInsert.size() >= 5000) {
106
+ insert logsToInsert;
107
+ logsToInsert.clear();
108
+ }
109
+
110
+ offset += pageSize;
111
+ }
112
+
113
+ if (!logsToInsert.isEmpty()) insert logsToInsert;
114
+ } finally {
115
+ cursor.close();
116
+ }
117
+
118
+ return new AuditSummary(totalCount, totalRevenue);
119
+ }
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Async Cursor Chaining with Queueable
126
+
127
+ Serialize a Cursor by ID and pass it across Queueable jobs to process 50M records across chained async transactions.
128
+
129
+ ```apex
130
+ public class LargeLeadProcessorQueueable implements Queueable {
131
+
132
+ private String cursorId;
133
+ private Integer offset;
134
+ private static final Integer PAGE_SIZE = 2000;
135
+
136
+ // First call — no cursor yet
137
+ public LargeLeadProcessorQueueable() {
138
+ Database.Cursor cursor = Database.getCursor(
139
+ 'SELECT Id, Status, LeadSource FROM Lead WHERE IsConverted = false ORDER BY Id'
140
+ );
141
+ this.cursorId = cursor.getId();
142
+ this.offset = 0;
143
+ cursor.close(); // Close handle; cursor remains alive on server
144
+ }
145
+
146
+ // Subsequent calls — resume from cursor
147
+ public LargeLeadProcessorQueueable(String cursorId, Integer offset) {
148
+ this.cursorId = cursorId;
149
+ this.offset = offset;
150
+ }
151
+
152
+ public void execute(QueueableContext ctx) {
153
+ Database.Cursor cursor = Database.getCursor(this.cursorId);
154
+
155
+ if (this.offset >= cursor.getNumRecords()) {
156
+ cursor.close();
157
+ return;
158
+ }
159
+
160
+ List<Lead> page = cursor.fetch(this.offset, PAGE_SIZE);
161
+ processLeads(page);
162
+
163
+ Integer nextOffset = this.offset + page.size();
164
+
165
+ if (nextOffset < cursor.getNumRecords()) {
166
+ cursor.close();
167
+ System.enqueueJob(new LargeLeadProcessorQueueable(this.cursorId, nextOffset));
168
+ } else {
169
+ cursor.close();
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## PaginationCursor for LWC / @AuraEnabled
178
+
179
+ For user-facing pagination (LWC infinite scroll, Screen Flows), use `Database.PaginationCursor`. It is `@AuraEnabled` compatible.
180
+
181
+ ```apex
182
+ public with sharing class AccountPaginationController {
183
+
184
+ private static final Integer DEFAULT_PAGE_SIZE = 20;
185
+
186
+ @AuraEnabled(cacheable=false)
187
+ public static PageResult getAccounts(String cursorId, Integer pageSize) {
188
+ if (pageSize == null || pageSize <= 0) pageSize = DEFAULT_PAGE_SIZE;
189
+
190
+ Database.PaginationCursor cursor;
191
+ if (String.isBlank(cursorId)) {
192
+ cursor = Database.getPaginationCursor(
193
+ 'SELECT Id, Name, Industry, AnnualRevenue FROM Account ORDER BY Name'
194
+ );
195
+ } else {
196
+ cursor = Database.getPaginationCursor(cursorId);
197
+ }
198
+
199
+ List<Account> page = cursor.fetch(pageSize);
200
+
201
+ PageResult result = new PageResult();
202
+ result.records = page;
203
+ result.cursorId = cursor.getId();
204
+ result.hasMore = cursor.hasMore();
205
+ result.totalCount = cursor.getNumRecords();
206
+ return result;
207
+ }
208
+
209
+ public class PageResult {
210
+ @AuraEnabled public List<Account> records;
211
+ @AuraEnabled public String cursorId;
212
+ @AuraEnabled public Boolean hasMore;
213
+ @AuraEnabled public Integer totalCount;
214
+ }
215
+ }
216
+ ```
217
+
218
+ ### LWC Integration
219
+
220
+ ```javascript
221
+ import { LightningElement } from 'lwc';
222
+ import getAccounts from '@salesforce/apex/AccountPaginationController.getAccounts';
223
+
224
+ export default class AccountInfiniteList extends LightningElement {
225
+ accounts = [];
226
+ cursorId = null;
227
+ hasMore = true;
228
+ isLoading = false;
229
+
230
+ connectedCallback() { this.loadPage(); }
231
+
232
+ async loadPage() {
233
+ if (this.isLoading || !this.hasMore) return;
234
+ this.isLoading = true;
235
+ try {
236
+ const result = await getAccounts({ cursorId: this.cursorId, pageSize: 20 });
237
+ this.accounts = [...this.accounts, ...result.records];
238
+ this.cursorId = result.cursorId;
239
+ this.hasMore = result.hasMore;
240
+ } catch (error) {
241
+ console.error('Pagination error:', error);
242
+ } finally {
243
+ this.isLoading = false;
244
+ }
245
+ }
246
+
247
+ handleLoadMore() { this.loadPage(); }
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Cursor Limits
254
+
255
+ | Constraint | Detail |
256
+ |-----------|--------|
257
+ | Max records per cursor | 50,000,000 |
258
+ | Cursor lifetime (sync) | 10 minutes |
259
+ | Cursor lifetime (async) | 60 minutes |
260
+ | `fetch()` max page size | 2,000 rows per call |
261
+ | Max open cursors | 10 per transaction |
262
+ | `PaginationCursor` @AuraEnabled | Fully supported |
263
+
264
+ ### Error Handling
265
+
266
+ Always close cursors in a `try/finally` block.
267
+
268
+ ```apex
269
+ Database.Cursor cursor = Database.getCursor('SELECT Id FROM Lead');
270
+ try {
271
+ List<Lead> leads = cursor.fetch(0, 100);
272
+ processLeads(leads);
273
+ } catch (System.CursorException e) {
274
+ // Cursor expired or already closed
275
+ System.debug(LoggingLevel.WARN, 'Cursor error: ' + e.getMessage());
276
+ throw;
277
+ } finally {
278
+ try { cursor.close(); } catch (Exception ignored) {}
279
+ }
280
+ ```
281
+
282
+ | Error | Cause | Fix |
283
+ |-------|-------|-----|
284
+ | `Cursor has been closed` | `fetch()` after `close()` | Check cursor state before fetching |
285
+ | `Cursor has expired` | > 10 min sync / 60 min async | Process faster or use Queueable chaining |
286
+ | `Maximum cursors exceeded` | > 10 open cursors | Close cursors in `finally` blocks |
287
+ | `Non-selective query` | WHERE clause not indexed | Add custom index or narrow query |
288
+
289
+ ---
290
+
291
+ ## Testing Cursor Code
292
+
293
+ ```apex
294
+ @IsTest
295
+ private class CursorPaginationTest {
296
+
297
+ @TestSetup
298
+ static void makeData() {
299
+ List<Account> accounts = new List<Account>();
300
+ for (Integer i = 0; i < 500; i++) {
301
+ accounts.add(new Account(Name = 'Cursor Test ' + String.valueOf(i).leftPad(3, '0')));
302
+ }
303
+ insert accounts;
304
+ }
305
+
306
+ @IsTest
307
+ static void shouldPaginateThroughAllRecords() {
308
+ Test.startTest();
309
+ Database.Cursor cursor = Database.getCursor(
310
+ 'SELECT Id, Name FROM Account WHERE Name LIKE \'Cursor Test%\' ORDER BY Name'
311
+ );
312
+ Integer totalFetched = 0;
313
+ try {
314
+ while (totalFetched < cursor.getNumRecords()) {
315
+ List<Account> page = (List<Account>) cursor.fetch(totalFetched, 200);
316
+ totalFetched += page.size();
317
+ }
318
+ } finally {
319
+ cursor.close();
320
+ }
321
+ Test.stopTest();
322
+ System.assertEquals(500, totalFetched, 'Should fetch all 500 records');
323
+ }
324
+ }
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Related
330
+
331
+ - **Skills**: `sf-apex-async-patterns` — For Queueable chaining patterns
332
+
333
+ ### Guardrails
334
+
335
+ - `sf-apex-constraints` — Governs SOQL and DML usage in cursor-processing code
336
+ - `sf-soql-constraints` — Governs query structure and selectivity requirements
@@ -0,0 +1,344 @@
1
+ ---
2
+ name: sf-apex-enterprise-patterns
3
+ description: >-
4
+ Use when implementing Salesforce Apex Enterprise Patterns (FFLIB) — Selector, Domain, Service, Unit of Work layers. Do NOT use for simple orgs or constraints.
5
+ ---
6
+
7
+ # Apex Enterprise Patterns
8
+
9
+ 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`.
10
+
11
+ Reference: @../_reference/ENTERPRISE_PATTERNS.md
12
+
13
+ ---
14
+
15
+ ## When to Use
16
+
17
+ - When building Apex applications that will scale beyond 5 developers or 50 custom classes
18
+ - When trigger logic is becoming complex and duplicated across multiple contexts
19
+ - When SOQL queries are scattered throughout classes instead of centralized
20
+ - When implementing FFLIB (Andy Fawcett) patterns in a Salesforce project
21
+ - When separating business logic from trigger context to improve testability
22
+ - When a service method needs to coordinate inserts, updates, and deletes atomically
23
+
24
+ ## When NOT to Use
25
+
26
+ - **Simple automations**: A before-insert trigger that sets a default status
27
+ - **One-off scripts**: Data migration or fix scripts
28
+ - **Small orgs** (< 5 developers, < 50 custom classes)
29
+ - **Read-only visualizations**: A Selector is often sufficient
30
+
31
+ The rule: introduce a layer when the absence of that layer is causing a real problem.
32
+
33
+ ---
34
+
35
+ ## Architecture Overview
36
+
37
+ ```
38
+ Trigger / Controller / API
39
+ |
40
+ Service Layer <- Transaction boundary, orchestration
41
+ |
42
+ Domain Layer <- Business rules on record collections
43
+ |
44
+ Selector Layer <- All SOQL queries
45
+ |
46
+ Unit of Work <- All DML (atomic commit)
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Selector Layer
52
+
53
+ Selectors own all SOQL queries for an object. No SOQL appears outside a Selector.
54
+
55
+ **Naming:** `{ObjectNamePlural}Selector` — e.g., `AccountsSelector`, `OpportunitiesSelector`
56
+
57
+ ### Without FFLIB
58
+
59
+ ```apex
60
+ public with sharing class AccountsSelector {
61
+
62
+ @TestVisible
63
+ private static AccountsSelector instance;
64
+
65
+ public static AccountsSelector newInstance() {
66
+ if (instance == null) instance = new AccountsSelector();
67
+ return instance;
68
+ }
69
+
70
+ public List<Account> selectById(Set<Id> accountIds) {
71
+ return [
72
+ SELECT Id, Name, Type, OwnerId, AnnualRevenue,
73
+ Customer_Tier__c, CreditLimit__c
74
+ FROM Account WHERE Id IN :accountIds
75
+ WITH USER_MODE ORDER BY Name
76
+ ];
77
+ }
78
+
79
+ public List<Account> selectWithOpenOpportunitiesById(Set<Id> accountIds) {
80
+ return [
81
+ SELECT Id, Name, AnnualRevenue, Customer_Tier__c,
82
+ (SELECT Id, Name, Amount, CloseDate, StageName
83
+ FROM Opportunities WHERE IsClosed = false
84
+ ORDER BY CloseDate ASC)
85
+ FROM Account WHERE Id IN :accountIds WITH USER_MODE
86
+ ];
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### With FFLIB
92
+
93
+ ```apex
94
+ public with sharing class AccountsSelector extends fflib_SObjectSelector {
95
+
96
+ public static AccountsSelector newInstance() {
97
+ return (AccountsSelector) Application.Selector.newInstance(Account.SObjectType);
98
+ }
99
+
100
+ public Schema.SObjectType getSObjectType() { return Account.SObjectType; }
101
+
102
+ public List<Schema.SObjectField> getSObjectFieldList() {
103
+ return new List<Schema.SObjectField>{
104
+ Account.Id, Account.Name, Account.Type,
105
+ Account.OwnerId, Account.AnnualRevenue
106
+ };
107
+ }
108
+
109
+ public List<Account> selectById(Set<Id> accountIds) {
110
+ return (List<Account>) selectSObjectsById(accountIds);
111
+ }
112
+ }
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Domain Layer
118
+
119
+ Encapsulates all business logic for a collection of records of the same type. Replaces trigger logic.
120
+
121
+ **Naming:** `{ObjectNamePlural}` — e.g., `Accounts`, `Opportunities`
122
+
123
+ ```apex
124
+ public with sharing class Accounts {
125
+
126
+ private final List<Account> records;
127
+ private final Map<Id, Account> existingRecords;
128
+
129
+ public static Accounts newInstance(List<Account> records) {
130
+ return new Accounts(records, null);
131
+ }
132
+
133
+ public static Accounts newInstance(List<Account> records, Map<Id, Account> existing) {
134
+ return new Accounts(records, existing);
135
+ }
136
+
137
+ private Accounts(List<Account> records, Map<Id, Account> existingRecords) {
138
+ this.records = records;
139
+ this.existingRecords = existingRecords;
140
+ }
141
+
142
+ public void onBeforeInsert() {
143
+ setDefaultCustomerTier();
144
+ validateRequiredFields();
145
+ }
146
+
147
+ public void onBeforeUpdate() {
148
+ validateRequiredFields();
149
+ preventDowngradingPremiumTier();
150
+ }
151
+
152
+ public void setDefaultCustomerTier() {
153
+ for (Account acc : records) {
154
+ if (String.isBlank(acc.Customer_Tier__c)) acc.Customer_Tier__c = 'Standard';
155
+ }
156
+ }
157
+
158
+ public void validateRequiredFields() {
159
+ for (Account acc : records) {
160
+ if (acc.Type == 'Customer' && String.isBlank(acc.Industry)) {
161
+ acc.Industry.addError('Industry is required for Customer account type.');
162
+ }
163
+ }
164
+ }
165
+
166
+ public void preventDowngradingPremiumTier() {
167
+ for (Account acc : records) {
168
+ Account existing = existingRecords?.get(acc.Id);
169
+ if (existing == null) continue;
170
+ if (existing.Customer_Tier__c == 'Premium'
171
+ && acc.Customer_Tier__c != 'Premium') {
172
+ acc.Customer_Tier__c.addError(
173
+ 'Premium tier downgrade requires approval.'
174
+ );
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### Trigger Using Domain Layer
182
+
183
+ ```apex
184
+ trigger AccountTrigger on Account (
185
+ before insert, before update, after insert, after update
186
+ ) {
187
+ if (Trigger.isBefore && Trigger.isInsert) {
188
+ Accounts.newInstance(Trigger.new).onBeforeInsert();
189
+ } else if (Trigger.isBefore && Trigger.isUpdate) {
190
+ Accounts.newInstance(Trigger.new, Trigger.oldMap).onBeforeUpdate();
191
+ }
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Service Layer
198
+
199
+ Orchestrates business processes that span multiple objects or require a full transaction boundary.
200
+
201
+ **Naming:** `{ObjectNamePlural}Service` — e.g., `AccountsService`
202
+
203
+ **Rules:**
204
+
205
+ 1. Static methods only — services are stateless
206
+ 2. No SOQL — delegate to Selectors
207
+ 3. No direct DML — use Unit of Work
208
+ 4. Owns the transaction boundary
209
+ 5. Calls Domain methods for record-level rules
210
+
211
+ ```apex
212
+ public with sharing class AccountsService {
213
+
214
+ public static void upgradeToPremium(Set<Id> accountIds) {
215
+ List<Account> accounts = AccountsSelector.newInstance()
216
+ .selectWithOpenOpportunitiesById(accountIds);
217
+
218
+ if (accounts.isEmpty()) {
219
+ throw new UpgradeException('No accounts found for IDs: ' + accountIds);
220
+ }
221
+
222
+ // Validate
223
+ List<String> errors = validateForUpgrade(accounts);
224
+ if (!errors.isEmpty()) {
225
+ throw new UpgradeException(String.join(errors, '\n'));
226
+ }
227
+
228
+ // Build Unit of Work
229
+ fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
230
+ for (Account acc : accounts) {
231
+ acc.Customer_Tier__c = 'Premium';
232
+ acc.CreditLimit__c = 100000.00;
233
+ uow.registerDirty(acc);
234
+
235
+ uow.registerNew(new Opportunity(
236
+ Name = acc.Name + ' - Premium Welcome',
237
+ AccountId = acc.Id,
238
+ StageName = 'Qualification',
239
+ CloseDate = Date.today().addDays(30)
240
+ ));
241
+ }
242
+
243
+ uow.commitWork(); // One atomic DML transaction
244
+ }
245
+
246
+ public class UpgradeException extends Exception {}
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Unit of Work
253
+
254
+ Accumulates all DML operations and commits them in a single, ordered, atomic transaction.
255
+
256
+ ### Lightweight Implementation (No FFLIB)
257
+
258
+ ```apex
259
+ public class SimpleUnitOfWork {
260
+
261
+ private List<SObject> toInsert = new List<SObject>();
262
+ private List<SObject> toUpdate = new List<SObject>();
263
+ private List<SObject> toDelete = new List<SObject>();
264
+
265
+ public void registerNew(SObject record) { toInsert.add(record); }
266
+ public void registerDirty(SObject record) { toUpdate.add(record); }
267
+ public void registerDeleted(SObject record) { toDelete.add(record); }
268
+
269
+ public void commitWork() {
270
+ Savepoint sp = Database.setSavepoint();
271
+ try {
272
+ if (!toInsert.isEmpty()) insert toInsert;
273
+ if (!toUpdate.isEmpty()) update toUpdate;
274
+ if (!toDelete.isEmpty()) delete toDelete;
275
+ } catch (Exception e) {
276
+ Database.rollback(sp);
277
+ throw e;
278
+ }
279
+ }
280
+ }
281
+ ```
282
+
283
+ ### FFLIB Application Factory
284
+
285
+ ```apex
286
+ public class Application {
287
+ public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
288
+ new fflib_Application.UnitOfWorkFactory(
289
+ new List<SObjectType>{
290
+ Account.SObjectType,
291
+ Contact.SObjectType,
292
+ Opportunity.SObjectType
293
+ }
294
+ );
295
+
296
+ public static final fflib_Application.SelectorFactory Selector =
297
+ new fflib_Application.SelectorFactory(
298
+ new Map<SObjectType, Type>{
299
+ Account.SObjectType => AccountsSelector.class,
300
+ Opportunity.SObjectType => OpportunitiesSelector.class
301
+ }
302
+ );
303
+ }
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Pragmatic Adoption Path
309
+
310
+ ### Phase 1: Selector + Service (Most Immediate Value)
311
+
312
+ Centralize SOQL into Selectors, business processes into Services. No FFLIB dependency needed.
313
+
314
+ ### Phase 2: Add Domain Layer for Trigger Logic
315
+
316
+ When trigger logic grows beyond simple field defaults, introduce the Domain layer.
317
+
318
+ ### Phase 3: Add Unit of Work for Complex Transactions
319
+
320
+ When a service needs to insert/update multiple related objects, introduce UoW for atomicity.
321
+
322
+ ---
323
+
324
+ ## FFLIB Installation
325
+
326
+ ```bash
327
+ # Clone and deploy FFLIB
328
+ git clone https://github.com/apex-enterprise-patterns/fflib-apex-common.git
329
+ git clone https://github.com/apex-enterprise-patterns/fflib-apex-mocks.git
330
+ sf project deploy start --source-dir fflib-apex-common/sfdx-source --target-org my-org
331
+ sf project deploy start --source-dir fflib-apex-mocks/sfdx-source --target-org my-org
332
+ ```
333
+
334
+ > FFLIB is typically deployed as unmanaged source code directly from the cloned repositories, not as a versioned managed package.
335
+
336
+ ---
337
+
338
+ ## Related
339
+
340
+ - **Agents**: `sf-review-agent`, `sf-architect` — For interactive guidance
341
+
342
+ ### Guardrails
343
+
344
+ - `sf-apex-constraints` — Governs all Apex code including enterprise pattern implementations