Skip to content

Commit b0b661e

Browse files
jasonsidersclaude[bot]claudeJason Siders
authored
Add TYPEOF query support for polymorphic fields (#140)
* rough implementation complete * cleanup * found a fix to coverage cap in databaselayerutils * updating threshold to 100% * reorering elseSelect method overloads * Generate wiki documentation for new TYPEOF query classes - Add The-Soql.TypeOf-Class.md with complete API documentation - Add The-Soql.WhenClause-Class.md with builder pattern documentation - Add _Sidebar.md with organized navigation structure - Include usage examples and cross-references 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * allow claude to trigger workflow * fixing relationships issue TYPEOF * Merge 'main' into 'soql-typeof-support' (#146) * Wiki restructuring and automated workflow improvements (#138) * Move wiki from submodule to main repository - Remove git submodule dependency for wiki - Add wiki content directly to main repository - Enable wiki changes to be included in same PR as code changes * updating release.yml to increment patch version by default * new action to sync the wiki * adding action to auto-document * Add wiki files to repository - Include all wiki content as regular files in main repository - Complete removal of submodule configuration * Add complete current wiki documentation - Include all current wiki content from apex-database-layer.wiki - Files will be synced to GitHub wiki via automated workflow - Enables wiki changes to be reviewed in PRs alongside code * Restore LICENSE.md and ensure README.md exists in both locations - Add back LICENSE.md that was accidentally removed - Copy README.md to wiki/ to maintain identical content in both locations * Remove README.md from wiki directory - README.md belongs in main repository, not in wiki - Wiki content should focus on documentation files only * Simplify sync-wiki workflow output - Remove unnecessary Summary step - Add single echo statement to Sync Wiki Files step - Use concise message about wiki sync completion * Fix Claude Code Review workflow authentication - Add github_token parameter to claude-code-action - Resolves workflow validation error when adding new workflow files in PRs - Enables Claude code review to work properly during PR validation * Fix Claude Code Review permissions - Change job-level permissions from read to write for pull-requests and issues - Enables Claude action to create comments and reviews on PRs - Resolves 'Resource not accessible by integration' error * Fix critical syntax error in auto-documentation workflow - Properly close git commit message with quotes - Separate git push command from commit message - Resolves YAML syntax error that would break the workflow * fixing wiki discrepancies * adding Home.md <> REAMDE sync * Fix wiki sync permissions issue (#141) - Add proper permissions (contents: write, pages: write, id-token: write) - Support fallback to WIKI_TOKEN secret for wiki repository access - Add better error handling and logging - Set explicit authentication URL for wiki repository push - Add validation that wiki directory exists before sync Resolves sync-wiki.yml permission denied error when pushing to wiki repository. * adding concurrency (#142) * Generate wiki documentation for new TYPEOF query classes - Add comprehensive documentation for Soql.TypeOf class - Add complete documentation for Soql.WhenClause class - Update _Sidebar.md with new class references in alphabetical order - Include usage examples, method signatures, and cross-references 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Jason Siders <undefined@users.noreply.github.com> * fixing documentation script to skip committing temporary changed_files.txt --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jason Siders <undefined@users.noreply.github.com>
1 parent 898e08e commit b0b661e

File tree

8 files changed

+749
-17
lines changed

8 files changed

+749
-17
lines changed

.github/workflows/auto-documentation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,9 @@ jobs:
139139
- name: Commit Documentation Updates
140140
if: steps.check-auto-commit.outputs.auto-commit == 'false' && steps.check-docs.outputs.has-doc-changes == 'true'
141141
run: |
142+
rm -f changed_files.txt
142143
git config --local user.email "action@github.com"
143144
git config --local user.name "GitHub Action"
144-
145145
git add wiki/ README.md
146146
git commit -m "Auto-generate documentation for code changes
147147

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
id: claude-review
5353
uses: anthropics/claude-code-action@beta
5454
with:
55+
allowed_bots: 'claude'
5556
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
5657
github_token: ${{ secrets.GITHUB_TOKEN }}
5758
direct_prompt: |
@@ -208,8 +209,8 @@ jobs:
208209
# Parse the JSON coverage summary file to extract overall coverage percentage
209210
COVERAGE_PERCENT=$(cat coverage/coverage/coverage-summary.json | jq -r '.total.lines.pct')
210211
echo "Code coverage: ${COVERAGE_PERCENT}%"
211-
if (( $(echo "$COVERAGE_PERCENT < 95" | bc -l) )); then
212-
echo "❌ Code coverage is ${COVERAGE_PERCENT}%, but 95% or higher is required"
212+
if (( $(echo "$COVERAGE_PERCENT < 100" | bc -l) )); then
213+
echo "❌ Code coverage is ${COVERAGE_PERCENT}%, but 100% is required"
213214
exit 1
214215
fi
215216

source/classes/DatabaseLayerUtilsTest.cls

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,22 @@ private class DatabaseLayerUtilsTest {
118118
Assert.isNotNull(example, 'Did not generate an object from CMDT');
119119
}
120120

121+
@IsTest
122+
static void shouldReturnNullPluginIfPluginNotConstructable() {
123+
DatabaseLayerSetting__mdt mockSetting = new DatabaseLayerSetting__mdt(
124+
DeveloperName = 'Test_123',
125+
DmlPreAndPostProcessor__c = 'Soql' // No public 0-arg constructor
126+
);
127+
MockCmdt.mock(DatabaseLayerSetting__mdt.SObjectType)?.add(mockSetting);
128+
SObjectField field = DatabaseLayerSetting__mdt.DmlPreAndPostProcessor__c;
129+
130+
Test.startTest();
131+
Object plugin = DatabaseLayer.Utils.Plugins.initPlugin(field);
132+
Test.stopTest();
133+
134+
Assert.isNull(plugin, 'Did not return null plugin');
135+
}
136+
121137
@IsTest
122138
static void shouldReturnNullPluginIfPluginInvalid() {
123139
DatabaseLayerSetting__mdt mockSetting = new DatabaseLayerSetting__mdt(

source/classes/Soql.cls

Lines changed: 272 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,15 +1572,16 @@ global inherited sharing virtual class Soql extends Soql.Builder {
15721572
*/
15731573
@SuppressWarnings('PMD.ExcessiveParameterList')
15741574
global class ParentField implements Soql.Selectable {
1575-
private String value;
1575+
private String fieldReference;
1576+
private String relationshipReference;
15761577

15771578
/**
15781579
* @description Constructor that creates a parent field reference from a chain of relationship fields.
15791580
* @param relationshipFieldChain List of SObjectFields representing the relationship chain
15801581
*/
15811582
global ParentField(List<SObjectField> relationshipFieldChain) {
15821583
relationshipFieldChain = relationshipFieldChain ?? new List<SObjectField>();
1583-
this.value = this.computeValue(relationshipFieldChain);
1584+
this.computeReferences(relationshipFieldChain);
15841585
}
15851586

15861587
/**
@@ -1665,32 +1666,46 @@ global inherited sharing virtual class Soql extends Soql.Builder {
16651666
this(new List<SObjectField>{ relationship1, field });
16661667
}
16671668

1669+
/**
1670+
* @description Outputs the SOQL relationship of the compound field
1671+
* @return The dot-notation relationship reference string
1672+
*/
1673+
global String getRelationshipName() {
1674+
return this.relationshipReference;
1675+
}
1676+
16681677
global override String toString() {
1669-
return this.value;
1678+
return this.fieldReference;
16701679
}
16711680

16721681
/**
1673-
* @description Computes the dot-notation string representation from the field chain.
1682+
* @description Computes dot-notation string representations from the field chain.
16741683
* @param fields List of fields in the relationship chain
1675-
* @return The dot-notation field reference string
16761684
*/
1677-
private String computeValue(List<SObjectField> fields) {
1678-
// Generate the String reference to the parent field:
1685+
private void computeReferences(List<SObjectField> fields) {
1686+
// Generate the references to the parent field based on the ordered chain:
16791687
List<String> components = new List<String>();
1688+
String targetFieldRelationshipName;
16801689
Integer size = fields?.size();
1681-
Integer lastIteration = size - 1;
1690+
Integer lastIndex = size - 1;
16821691
for (Integer i = 0; i < size; i++) {
16831692
SObjectField field = fields?.get(i);
1684-
if (i < lastIteration) {
1685-
String relationshipName = field?.getDescribe()?.getRelationshipName();
1693+
String relationshipName = field?.getDescribe()?.getRelationshipName();
1694+
if (i < lastIndex) {
16861695
components?.add(relationshipName);
16871696
} else {
1688-
// Final field in the chain - Get the API name of the field
1689-
String fieldName = field?.toString();
1690-
components?.add(fieldName);
1697+
// Final field in the chain:
1698+
String targetFieldApiName = field?.toString();
1699+
components?.add(targetFieldApiName);
1700+
targetFieldRelationshipName = relationshipName;
16911701
}
16921702
}
1693-
return String.join(components, '.');
1703+
// Compute the compound field's API name
1704+
this.fieldReference = String.join(components, '.');
1705+
// Compute the compound field's relationship name
1706+
components?.remove(lastIndex);
1707+
components?.add(targetFieldRelationshipName);
1708+
this.relationshipReference = String.join(components, '.');
16941709
}
16951710
}
16961711

@@ -2003,4 +2018,247 @@ global inherited sharing virtual class Soql extends Soql.Builder {
20032018
return this.relationshipName;
20042019
}
20052020
}
2021+
2022+
/**
2023+
* @description Represents a TYPEOF clause for polymorphic field queries in SOQL.
2024+
*/
2025+
global class TypeOf implements Soql.Selectable {
2026+
private List<String> elseFields;
2027+
private String relationshipName;
2028+
private List<String> whenClauses;
2029+
2030+
/**
2031+
* @description Constructor for TypeOf with relationship name.
2032+
* @param relationshipName The polymorphic field name to query
2033+
*/
2034+
global TypeOf(String relationshipName) {
2035+
this.relationshipName = relationshipName;
2036+
this.whenClauses = new List<String>{};
2037+
}
2038+
2039+
/**
2040+
* @description Constructor for TypeOf with SObjectField.
2041+
* @param field The polymorphic SObjectField to query
2042+
*/
2043+
global TypeOf(SObjectField field) {
2044+
this(field?.getDescribe()?.getRelationshipName());
2045+
}
2046+
2047+
/**
2048+
* @description Constructor for TypeOf with parent field.
2049+
* @param field The polymorphic parent field to query
2050+
*/
2051+
global TypeOf(Soql.ParentField field) {
2052+
this(field?.getRelationshipName());
2053+
}
2054+
2055+
/**
2056+
* @description Adds a WHEN clause and returns a builder for specifying fields.
2057+
* @param objectType The SObjectType to match in the WHEN clause
2058+
* @return A WhenClause builder for specifying the fields to select
2059+
*/
2060+
global Soql.WhenClause when(SObjectType objectType) {
2061+
return new Soql.WhenClause(this, objectType);
2062+
}
2063+
2064+
/**
2065+
* @description Adds an ELSE clause with multiple field names.
2066+
* @param fieldNames List of field names to select in the ELSE clause
2067+
* @return This TypeOf instance for method chaining
2068+
*/
2069+
global Soql.TypeOf elseSelect(List<String> fieldNames) {
2070+
this.elseFields = fieldNames;
2071+
return this;
2072+
}
2073+
2074+
/**
2075+
* @description Adds an ELSE clause with up to 5 field names.
2076+
* @param field1 First field name
2077+
* @param field2 Second field name
2078+
* @param field3 Third field name
2079+
* @param field4 Fourth field name
2080+
* @param field5 Fifth field name
2081+
* @return This TypeOf instance for method chaining
2082+
*/
2083+
@SuppressWarnings('PMD.ExcessiveParameterList')
2084+
global Soql.TypeOf elseSelect(String field1, String field2, String field3, String field4, String field5) {
2085+
List<String> fields = new List<String>();
2086+
for (String field : new List<String>{ field1, field2, field3, field4, field5 }) {
2087+
if (String.isNotBlank(field)) {
2088+
fields.add(field);
2089+
}
2090+
}
2091+
return this.elseSelect(fields);
2092+
}
2093+
2094+
/**
2095+
* @description Adds an ELSE clause with 4 field names.
2096+
* @param field1 First field name
2097+
* @param field2 Second field name
2098+
* @param field3 Third field name
2099+
* @param field4 Fourth field name
2100+
* @return This TypeOf instance for method chaining
2101+
*/
2102+
@SuppressWarnings('PMD.ExcessiveParameterList')
2103+
global Soql.TypeOf elseSelect(String field1, String field2, String field3, String field4) {
2104+
return this.elseSelect(field1, field2, field3, field4, null);
2105+
}
2106+
2107+
/**
2108+
* @description Adds an ELSE clause with 3 field names.
2109+
* @param field1 First field name
2110+
* @param field2 Second field name
2111+
* @param field3 Third field name
2112+
* @return This TypeOf instance for method chaining
2113+
*/
2114+
global Soql.TypeOf elseSelect(String field1, String field2, String field3) {
2115+
return this.elseSelect(field1, field2, field3, null);
2116+
}
2117+
2118+
/**
2119+
* @description Adds an ELSE clause with 2 field names.
2120+
* @param field1 First field name
2121+
* @param field2 Second field name
2122+
* @return This TypeOf instance for method chaining
2123+
*/
2124+
global Soql.TypeOf elseSelect(String field1, String field2) {
2125+
return this.elseSelect(field1, field2, null);
2126+
}
2127+
2128+
/**
2129+
* @description Adds an ELSE clause with a single field name.
2130+
* @param fieldName The field name to select in the ELSE clause
2131+
* @return This TypeOf instance for method chaining
2132+
*/
2133+
global Soql.TypeOf elseSelect(String fieldName) {
2134+
return this.elseSelect(fieldName, null);
2135+
}
2136+
2137+
global override String toString() {
2138+
// Construct a valid TYPEOF...WHEN...THEN...ELSE...END clause
2139+
List<String> parts = new List<String>();
2140+
parts.add('TYPEOF ' + this.relationshipName);
2141+
for (String whenClause : this.whenClauses) {
2142+
parts.add(whenClause);
2143+
}
2144+
if (this.elseFields != null && !this.elseFields.isEmpty()) {
2145+
String fields = String.join(this.elseFields, ', ');
2146+
parts.add('ELSE ' + fields);
2147+
}
2148+
parts.add('END');
2149+
return String.join(parts, ' ');
2150+
}
2151+
2152+
/**
2153+
* @description Internal method to add a when clause.
2154+
* @param objectType The SObjectType for this WHEN clause
2155+
* @param fieldNames List of field names to select
2156+
*/
2157+
private void addWhenClause(SObjectType objectType, List<String> fieldNames) {
2158+
String fields = String.join(fieldNames, ', ');
2159+
this.whenClauses.add('WHEN ' + objectType + ' THEN ' + fields);
2160+
}
2161+
}
2162+
2163+
/**
2164+
* @description Builder for WHEN clauses in TYPEOF queries.
2165+
*/
2166+
global class WhenClause {
2167+
private Soql.TypeOf parent;
2168+
private SObjectType objectType;
2169+
2170+
/**
2171+
* @description Constructor for WhenClause builder.
2172+
* @param parent The parent TypeOf instance
2173+
* @param objectType The SObjectType for this WHEN clause
2174+
*/
2175+
private WhenClause(Soql.TypeOf parent, SObjectType objectType) {
2176+
this.parent = parent;
2177+
this.objectType = objectType;
2178+
}
2179+
2180+
/**
2181+
* @description Specifies fields to select for this WHEN clause using a list of SObjectFields.
2182+
* @param fields List of SObjectFields to select
2183+
* @return The parent TypeOf instance for continued chaining
2184+
*/
2185+
global Soql.TypeOf thenSelect(List<SObjectField> fields) {
2186+
List<String> fieldNames = new List<String>();
2187+
for (SObjectField field : fields) {
2188+
if (field != null) {
2189+
fieldNames.add(field?.toString());
2190+
}
2191+
}
2192+
this.parent?.addWhenClause(this.objectType, fieldNames);
2193+
return this.parent;
2194+
}
2195+
2196+
/**
2197+
* @description Specifies up to 5 SObjectFields to select for this WHEN clause.
2198+
* @param field1 First SObjectField
2199+
* @param field2 Second SObjectField
2200+
* @param field3 Third SObjectField
2201+
* @param field4 Fourth SObjectField
2202+
* @param field5 Fifth SObjectField
2203+
* @return The parent TypeOf instance for continued chaining
2204+
*/
2205+
@SuppressWarnings('PMD.ExcessiveParameterList')
2206+
global Soql.TypeOf thenSelect(
2207+
SObjectField field1,
2208+
SObjectField field2,
2209+
SObjectField field3,
2210+
SObjectField field4,
2211+
SObjectField field5
2212+
) {
2213+
return this.thenSelect(new List<SObjectField>{ field1, field2, field3, field4, field5 });
2214+
}
2215+
2216+
/**
2217+
* @description Specifies 4 SObjectFields to select for this WHEN clause.
2218+
* @param field1 First SObjectField
2219+
* @param field2 Second SObjectField
2220+
* @param field3 Third SObjectField
2221+
* @param field4 Fourth SObjectField
2222+
* @return The parent TypeOf instance for continued chaining
2223+
*/
2224+
@SuppressWarnings('PMD.ExcessiveParameterList')
2225+
global Soql.TypeOf thenSelect(
2226+
SObjectField field1,
2227+
SObjectField field2,
2228+
SObjectField field3,
2229+
SObjectField field4
2230+
) {
2231+
return this.thenSelect(field1, field2, field3, field4, null);
2232+
}
2233+
2234+
/**
2235+
* @description Specifies 3 SObjectFields to select for this WHEN clause.
2236+
* @param field1 First SObjectField
2237+
* @param field2 Second SObjectField
2238+
* @param field3 Third SObjectField
2239+
* @return The parent TypeOf instance for continued chaining
2240+
*/
2241+
global Soql.TypeOf thenSelect(SObjectField field1, SObjectField field2, SObjectField field3) {
2242+
return this.thenSelect(field1, field2, field3, null);
2243+
}
2244+
2245+
/**
2246+
* @description Specifies 2 SObjectFields to select for this WHEN clause.
2247+
* @param field1 First SObjectField
2248+
* @param field2 Second SObjectField
2249+
* @return The parent TypeOf instance for continued chaining
2250+
*/
2251+
global Soql.TypeOf thenSelect(SObjectField field1, SObjectField field2) {
2252+
return this.thenSelect(field1, field2, null);
2253+
}
2254+
2255+
/**
2256+
* @description Specifies 1 SObjectField to select for this WHEN clause.
2257+
* @param field The SObjectField to select
2258+
* @return The parent TypeOf instance for continued chaining
2259+
*/
2260+
global Soql.TypeOf thenSelect(SObjectField field) {
2261+
return this.thenSelect(field, null);
2262+
}
2263+
}
20062264
}

0 commit comments

Comments
 (0)