Migration Guide: QueryBuilder → LambdaModel¶
Upgrade to type-safe queries with closure variable support
Why Migrate?¶
The old QueryBuilder and string-based Model class are deprecated and will be removed in v2.0.0.
What You Get¶
| Feature | QueryBuilder (Old) | LambdaModel (New) |
|---|---|---|
| Type Safety | ❌ No | ✅ Full IntelliSense |
| Closure Variables | ❌ No | ✅ Yes |
| Typo Detection | ❌ Runtime errors | ✅ Compile-time |
| Field Suggestions | ❌ No | ✅ IDE autocomplete |
| Subquery Closures | ❌ No | ✅ Yes |
| Future Support | ⚠️ Deprecated | ✅ Actively developed |
Step-by-Step Migration¶
Step 1: Update Your Imports¶
Before:
After:
Step 2: Change Base Class¶
Before:
After:
Step 3: Update Queries¶
Simple SELECT¶
Before:
After:
const accounts = await Account
.select(x => ({
Id: x.Id,
Name: x.Name,
Industry: x.Industry
}))
.get();
WHERE Clauses¶
Before:
const accounts = await Account
.select('Id', 'Name')
.where('Industry', 'Technology')
.where('AnnualRevenue', '>', 1000000)
.get();
After:
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.where(x => x.Industry === 'Technology' && x.AnnualRevenue > 1000000)
.get();
ORDER BY¶
Before:
After:
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.orderBy(x => x.AnnualRevenue, 'DESC')
.get();
Pagination¶
Before:
After:
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.limit(20)
.offset(40)
.get();
Step 4: Leverage Closure Variables¶
This is the biggest benefit - you can now use variables from outer scopes:
Before (workaround):
const industry = 'Technology';
const minRevenue = 1000000;
// Had to pass as values
const accounts = await Account
.select('Id', 'Name')
.where('Industry', industry)
.where('AnnualRevenue', '>', minRevenue)
.get();
After (natural):
const industry = 'Technology';
const minRevenue = 1000000;
// Variables just work!
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.where(x => x.Industry === industry && x.AnnualRevenue > minRevenue)
.get();
Step 5: Update Relationships¶
Before:
After:
const activeStatus = true;
const accounts = await Account
.select(x => ({
Name: x.Name,
ActiveContacts: x.Contacts
.select(c => ({ Name: c.Name, Email: c.Email }))
.where(c => c.Active__c === activeStatus) // Closure works!
}))
.get();
Common Migration Patterns¶
Pattern 1: whereIn() → includes() with arrays¶
Before:
const industries = ['Technology', 'Finance', 'Healthcare'];
const accounts = await Account
.select('Id', 'Name')
.whereIn('Industry', industries)
.get();
After (New!):
const industries = ['Technology', 'Finance', 'Healthcare'];
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.where(x => x.Industry.includes(industries))
.get();
// SOQL: WHERE Industry IN ('Technology', 'Finance', 'Healthcare')
The .includes() method automatically detects arrays and generates efficient WHERE IN clauses!
Pattern 2: whereGroup() → Parentheses¶
Before:
const accounts = await Account
.select('Id', 'Name')
.where('IsActive', true)
.orWhereGroup(qb => {
qb.where('Name', 'LIKE', '%John%')
.orWhere('Email', 'LIKE', '%john%');
})
.get();
After:
const accounts = await Account
.select(x => ({ Id: x.Id, Name: x.Name }))
.where(x =>
x.IsActive === true &&
(x.Name.includes('John') || x.Email.includes('john'))
)
.get();
Pattern 3: Dynamic Filters¶
Before:
function getAccounts(filters: any) {
let query = Account.select('Id', 'Name');
if (filters.industry) {
query = query.where('Industry', filters.industry);
}
if (filters.minRevenue) {
query = query.where('AnnualRevenue', '>', filters.minRevenue);
}
return query.get();
}
After:
function getAccounts(filters: any) {
let query = Account.select(x => ({ Id: x.Id, Name: x.Name }));
if (filters.industry) {
query = query.where(x => x.Industry === filters.industry);
}
if (filters.minRevenue) {
query = query.where(x => x.AnnualRevenue > filters.minRevenue);
}
return query.get();
}
Pattern 4: count()¶
Before:
After:
Troubleshooting¶
Issue: "Property does not exist on type"¶
Problem:
const accounts = await Account
.select(x => ({
Id: x.Id,
NonExistentField: x.NonExistentField // ❌ Error!
}))
.get();
Solution: This is actually good! TypeScript is catching a typo. Update your interface if the field exists:
interface AccountData extends ModelData {
Id: string;
Name: string;
NonExistentField: string; // Add the field
}
Issue: Closure variable not working¶
Problem:
// Still using old Model class
class Account extends Model<AccountData> { }
const industry = 'Tech';
const accounts = await Account
.select(x => ({ Id: x.Id }))
.where(x => x.Industry === industry); // ❌ Doesn't work with old Model
Solution: Make sure you're using LambdaModel:
Issue: Lost QueryBuilder methods¶
Problem:
Solution: Use the lambda WHERE syntax instead - it's more flexible:
// Instead of whereIn - use includes() with arrays:
const industries = ['Tech', 'Finance'];
.where(x => x.Industry.includes(industries))
// SOQL: WHERE Industry IN ('Tech', 'Finance')
// Instead of whereNotIn - negate the includes:
.where(x => !x.Industry.includes(industries))
// Or use AND conditions:
.where(x => x.Industry !== 'Tech' && x.Industry !== 'Finance')
// Instead of whereGroup:
.where(x => x.IsActive && (x.Type === 'A' || x.Type === 'B'))
Deprecation Timeline¶
| Version | Status | Notes |
|---|---|---|
| v1.x | ⚠️ Current | Both Model and LambdaModel available |
| v1.x (soon) | 🔔 Deprecation warnings | Console warnings when using old Model |
| v2.0 | 🚫 Breaking | Old Model & QueryBuilder removed |
| ✅ | LambdaModel renamed to Model |
Checklist¶
- [ ] Update imports:
Model→LambdaModel - [ ] Update base class:
extends Model→extends LambdaModel - [ ] Convert
.select('fields')→.select(x => ({ fields })) - [ ] Convert
.where('field', value)→.where(x => x.field === value) - [ ] Convert
.orderBy('field')→.orderBy(x => x.field) - [ ] Test with closure variables
- [ ] Update subqueries to use closures
- [ ] Remove any QueryBuilder workarounds
Need Help?¶
Start migrating today! 🚀