Closure Variables Deep Dive¶
Learn how closure variable support works in LambdaModel queries.
What Are Closures?¶
Closures allow lambda functions to access variables from outer scopes:
const industry = 'Technology'; // Variable in outer scope
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.where(x => x.Industry === industry) // ✓ Closure variable!
.get();
How It Works¶
LambdaModel uses two techniques to extract closure values:
- TypeScript AST Parsing (ts-morph) - Analyzes the lambda function structure
- Node.js Inspector Protocol - Extracts variable values at runtime
Technical Flow¶
const minRevenue = 1000000;
const query = Account.where(x => x.AnnualRevenue > minRevenue);
// 1. Parse lambda with ts-morph AST
// → Identifies "minRevenue" as a closure variable
// → Determines it's a comparison: AnnualRevenue > minRevenue
//
// 2. Extract value with Inspector Protocol
// → Accesses function's [[Scopes]] internal property
// → Retrieves minRevenue = 1000000
//
// 3. Build SOQL
// → WHERE AnnualRevenue > 1000000
Supported Closure Types¶
Simple Variables¶
const industry = 'Technology';
const minRevenue = 1000000;
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.where(x => x.Industry === industry && x.AnnualRevenue > minRevenue)
.get();
Object Properties¶
const config = {
industry: 'Technology',
minRevenue: 1000000
};
const accounts = await Account
.select(x => ({ Id: x.Id }))
.where(x => x.Industry === config.industry && x.AnnualRevenue > config.minRevenue)
.get();
Nested Object Properties¶
const filters = {
account: {
criteria: {
industry: 'Technology',
location: {
city: 'San Francisco'
}
}
}
};
const accounts = await Account
.select(x => ({ Id: x.Id }))
.where(x =>
x.Industry === filters.account.criteria.industry &&
x.BillingCity === filters.account.criteria.location.city
)
.get();
Closures in Subqueries¶
Closures work in subquery WHERE clauses:
const activeStatus = true;
const minAmount = 50000;
const accounts = await Account
.select(x => ({
Name: x.Name,
ActiveContacts: x.Contacts
.select(c => ({ Name: c.Name }))
.where(c => c.Active__c === activeStatus), // ✓ Closure works!
BigOpportunities: x.Opportunities
.select(o => ({ Name: o.Name, Amount: o.Amount }))
.where(o => o.Amount > minAmount) // ✓ Closure works!
}))
.get();
How Subquery Closures Work¶
- Selector Execution: When
.select()is called, the lambda is executed with a proxy object - Capture Phase: The proxy intercepts
.where()calls and stores the function with its closure context - Parse Phase: When building SOQL, the captured function is parsed with Inspector Protocol access intact
- SOQL Generation: Closure values are extracted and injected into the subquery
// Behind the scenes:
// 1. Proxy captures: x.Contacts.where(c => c.Active === activeStatus)
// 2. Function stored in Map with key "Contacts_where"
// 3. Parser retrieves function from Map (closure preserved)
// 4. Inspector extracts activeStatus = true
// 5. Generates: (SELECT Name FROM Contacts WHERE Active__c = true)
Limitations¶
Synchronous Only¶
The Inspector Protocol API is synchronous:
// ✓ Works - synchronous variable access
const industry = 'Technology';
const accounts = await Account
.where(x => x.Industry === industry)
.get();
// ❌ Won't work - async function calls not supported
const getIndustry = async () => 'Technology';
const accounts = await Account
.where(x => x.Industry === await getIndustry()) // Error!
.get();
No Function Calls¶
Only direct variable references work:
// ✓ Works
const filters = {
getIndustry: () => 'Technology'
};
// But you must call it OUTSIDE the lambda:
const industry = filters.getIndustry();
const accounts = await Account
.where(x => x.Industry === industry)
.get();
// ❌ Won't work - function call inside lambda
const accounts = await Account
.where(x => x.Industry === filters.getIndustry()) // Error!
.get();
Inspector Protocol Required¶
If the Node.js Inspector Protocol is unavailable, closures will fail silently:
// ✓ Fallback to literal parsing (if Inspector unavailable)
const accounts = await Account
.where(x => x.Industry === 'Technology') // Literal works
.get();
// ❌ Fails silently if Inspector unavailable
const industry = 'Technology';
const accounts = await Account
.where(x => x.Industry === industry) // Returns undefined, generates invalid SOQL
.get();
Best Practices¶
- Extract values before lambda - Don't call functions inside lambdas
- Use simple variable references - Avoid complex expressions
- Test with real data - Verify closures work in your environment
- Fallback to literals - If closures don't work, use literal values
Good Examples¶
// ✓ Simple variables
const industry = 'Technology';
const accounts = await Account.where(x => x.Industry === industry).get();
// ✓ Object properties
const config = { industry: 'Technology' };
const accounts = await Account.where(x => x.Industry === config.industry).get();
// ✓ Pre-computed values
const industries = ['Technology', 'Finance'];
const firstIndustry = industries[0];
const accounts = await Account.where(x => x.Industry === firstIndustry).get();
Bad Examples¶
// ❌ Function call inside lambda
const getIndustry = () => 'Technology';
const accounts = await Account
.where(x => x.Industry === getIndustry()) // Won't work!
.get();
// ❌ Async operations
const industry = await fetchIndustryFromDB();
const accounts = await Account
.where(x => x.Industry === await fetchIndustryFromDB()) // Won't work!
.get();
// ❌ Complex expressions
const config = { base: 'Tech', suffix: 'nology' };
const accounts = await Account
.where(x => x.Industry === config.base + config.suffix) // Won't work!
.get();
Debugging Closures¶
Use toSOQL() to verify closure values were extracted correctly:
const industry = 'Technology';
const query = Account
.select(x => ({ Id: x.Id }))
.where(x => x.Industry === industry);
console.log(query.toSOQL());
// Expected: SELECT Id FROM Account WHERE Industry = 'Technology'
// If you see: SELECT Id FROM Account WHERE Industry = undefined
// Then closure extraction failed!
Next Steps¶
- Lambda Queries - Full lambda query guide
- Advanced Queries - Complex patterns
- Error Handling - Handle errors gracefully