AsyncCollection Guide¶
Welcome to the AsyncCollection guide! This guide introduces the deferred query builder pattern for creating abstractions over any data source including ORMs, REST APIs, GraphQL, and more.
What is AsyncCollection?¶
AsyncCollection implements a Query Builder Pattern that constructs a "query plan" (array of operations) that executes when the promise resolves. This allows creating powerful abstractions over any data source by transforming operations into the specific format required by your ORM or API.
Key Features¶
- Deferred Execution: Operations are registered but not executed until you await or call
.then() - ORM Agnostic: Works with Prisma, TypeORM, Sequelize, Mongoose, or any data source
- Type Safe: Full TypeScript support with generics
- Chainable: Fluent interface for building complex queries
- Flexible: Transform operations to match your data source format
When to Use AsyncCollection¶
Use AsyncCollection when you need to:
- Create abstractions over database ORMs
- Build reusable query interfaces for REST APIs
- Implement custom data access layers
- Unify different data sources under a common interface
- Defer query execution until all filters are applied
Basic Concepts¶
The Executor Function¶
The executor is the core of AsyncCollection. It receives a context containing all operations and returns the results:
import AsyncCollection from "@arcaelas/collection/async";
const executor = async ({ operations, validators, metadata }) => {
// operations: Array of [method_name, ...args]
// validators: Custom validators (optional)
// metadata: Query info (created_at, operation_count, chain_depth)
console.log(`Processing ${metadata.operation_count} operations`);
// Transform operations to your format
// Execute query
// Return results
return results;
};
const collection = new AsyncCollection(executor);
Operation Registration¶
When you chain methods, they're registered as operations:
const users = new AsyncCollection(executor);
// These register operations but DON'T execute yet
users
.where('age', '>=', 18)
.where('status', 'active')
.sort('name', 'asc');
// Execution happens here (when promise resolves)
const results = await users;
ExecutorContext¶
The context passed to your executor contains:
interface ExecutorContext<T, V> {
// Array of operations: [method_name, ...args]
operations: [string, ...any[]][];
// Custom validators for query()
validators?: V;
// Metadata about the query
metadata: {
created_at: Date;
operation_count: number;
chain_depth: number;
};
}
Simple Example: In-Memory Array¶
Let's start with a simple example using an in-memory array:
import AsyncCollection from "@arcaelas/collection/async";
// In-memory data
const data = [
{ id: 1, name: "Alice", age: 25, status: "active" },
{ id: 2, name: "Bob", age: 30, status: "inactive" },
{ id: 3, name: "Charlie", age: 35, status: "active" }
];
// Simple executor
const users = new AsyncCollection(async ({ operations }) => {
let results = [...data];
operations.forEach(([method, ...args]) => {
if (method === 'where') {
const [key, operator, value] = args.length === 3
? args
: [args[0], '=', args[1]];
results = results.filter(item => {
if (operator === '>=') return item[key] >= value;
if (operator === '=') return item[key] === value;
return true;
});
}
if (method === 'first') {
results = [results[0]];
}
});
return results;
});
// Use it
const active = await users.where('status', 'active');
// [{ id: 1, ... }, { id: 3, ... }]
const firstAdult = await users.where('age', '>=', 25).first();
// { id: 1, name: "Alice", ... }
Working with Prisma¶
Transform operations to Prisma queries:
import AsyncCollection from "@arcaelas/collection/async";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const users = new AsyncCollection(async ({ operations }) => {
const where: any = {};
let orderBy: any = undefined;
let take: number | undefined;
operations.forEach(([method, ...args]) => {
if (method === 'where') {
const [key, operator, value] = args.length === 3
? args
: [args[0], '=', args[1]];
if (operator === '>=') where[key] = { gte: value };
else if (operator === '>') where[key] = { gt: value };
else if (operator === '=') where[key] = value;
}
if (method === 'sort') {
const [key, direction] = args;
orderBy = { [key]: direction || 'asc' };
}
});
const lastOp = operations[operations.length - 1];
if (lastOp && lastOp[0] === 'first') {
return await prisma.user.findFirst({ where, orderBy });
}
return await prisma.user.findMany({ where, orderBy, take });
});
// Use it like Collection
const active = await users
.where('age', '>=', 18)
.where('verified', true)
.sort('createdAt', 'desc');
const firstUser = await users
.where('status', 'active')
.first();
Working with TypeORM¶
Build TypeORM QueryBuilder from operations:
import AsyncCollection from "@arcaelas/collection/async";
import { getRepository } from "typeorm";
import { User } from "./entities/User";
const users = new AsyncCollection(async ({ operations }) => {
const qb = getRepository(User).createQueryBuilder('user');
operations.forEach(([method, ...args]) => {
if (method === 'where') {
const [key, operator, value] = args.length === 3
? args
: [args[0], '=', args[1]];
const paramKey = `${key}_${Math.random()}`;
if (operator === '>=') {
qb.andWhere(`user.${key} >= :${paramKey}`, { [paramKey]: value });
} else if (operator === '=') {
qb.andWhere(`user.${key} = :${paramKey}`, { [paramKey]: value });
}
}
if (method === 'sort') {
const [key, direction] = args;
qb.orderBy(`user.${key}`, direction?.toUpperCase() || 'ASC');
}
});
const lastOp = operations[operations.length - 1];
if (lastOp && lastOp[0] === 'first') {
return await qb.getOne();
}
return await qb.getMany();
});
// Chain operations
const result = await users
.where('age', '>=', 21)
.where('status', 'active')
.sort('name', 'asc');
Custom Validators¶
Extend query capabilities with custom validators:
const customValidators = {
// Check if date is in the past
$isPast(ref: string, value: boolean) {
return (item: any) => {
const date = new Date(item[ref]);
const isPast = date < new Date();
return value ? isPast : !isPast;
};
},
// Check if value is within range
$between(ref: string, range: [number, number]) {
return (item: any) => {
const val = item[ref];
return val >= range[0] && val <= range[1];
};
}
};
const items = new AsyncCollection(
async ({ operations, validators }) => {
// Use validators to process operations
// Transform to your data source format
return processData(operations, validators);
},
customValidators
);
// Use custom validators
await items.filter({
eventDate: { $isPast: true },
score: { $between: [50, 100] }
});
Method Chaining¶
Chain multiple operations fluently:
const users = new AsyncCollection(executor);
const result = await users
.where('age', '>=', 18)
.where('status', 'active')
.not({ deleted: true })
.sort('createdAt', 'desc')
.slice(0, 10)
.map(user => ({
id: user.id,
name: user.name,
displayName: `${user.name} (${user.age})`
}));
Supported Operations¶
AsyncCollection supports all Collection methods:
Filtering¶
where(key, value)orwhere(key, operator, value)whereNot(key, value)orwhereNot(key, operator, value)filter(handler)- Function or query objectnot(handler)- Inverse filter
Finding¶
first(handler?)- First matching elementlast(handler?)- Last matching elementfind(handler)- Alias for first
Transformation¶
map(handler)- Transform elementseach(handler)- Iterate elementsforget(...keys)- Remove fields
Sorting & Ordering¶
sort(key, direction)orsort(compareFunction)reverse()- Reverse ordershuffle()- Random order
Slicing & Pagination¶
slice(start, end?)- Array slicechunk(size)- Divide into chunkspaginate(page, perPage)- Paginate results
Aggregation¶
sum(key)- Sum valuesmax(key)- Maximum valuemin(key)- Minimum valuegroupBy(key)- Group by keycountBy(key)- Count by key
Utility¶
unique(key)- Unique elementsrandom(count?)- Random elementsevery(handler)- Validate allcollect(items?)- Clone context
Debugging¶
dump()- Print to consoledd()- Dump and die (Node.js)stringify(replacer?, space?)- Convert to JSON
Error Handling¶
Handle errors in your executor:
const users = new AsyncCollection(async ({ operations }) => {
try {
// Build and execute query
const results = await executeQuery(operations);
return results;
} catch (error) {
console.error('Query failed:', error);
throw new Error(`Failed to fetch users: ${error.message}`);
}
});
// Handle errors when awaiting
try {
const results = await users.where('age', '>=', 18);
} catch (error) {
console.error('Error:', error);
}
// Or with .catch()
users
.where('age', '>=', 18)
.then(results => console.log(results))
.catch(error => console.error(error));
Best Practices¶
1. Validate Operations¶
const users = new AsyncCollection(async ({ operations }) => {
// Validate operations before executing
const hasInvalidOp = operations.some(([method]) =>
!['where', 'sort', 'first'].includes(method)
);
if (hasInvalidOp) {
throw new Error('Unsupported operation');
}
return processOperations(operations);
});
2. Handle Terminal Operations¶
const users = new AsyncCollection(async ({ operations }) => {
const lastOp = operations[operations.length - 1];
// Check for terminal operations
if (lastOp && lastOp[0] === 'first') {
return await findFirst(operations.slice(0, -1));
}
if (lastOp && lastOp[0] === 'last') {
return await findLast(operations.slice(0, -1));
}
return await findMany(operations);
});
3. Optimize Query Building¶
const users = new AsyncCollection(async ({ operations }) => {
// Group operations by type for efficient processing
const where_ops = operations.filter(([m]) => m === 'where');
const sort_ops = operations.filter(([m]) => m === 'sort');
// Build optimized query
const query = buildQuery({ where_ops, sort_ops });
return await executeQuery(query);
});
Next Steps¶
- Check out API Reference for complete method documentation
- See Usage Examples for real-world patterns
- Learn about TypeScript Usage for type safety
Common Patterns¶
REST API Wrapper¶
const api = new AsyncCollection(async ({ operations }) => {
const params = new URLSearchParams();
operations.forEach(([method, ...args]) => {
if (method === 'where') {
const [key, value] = args.length === 2 ? args : [args[0], args[2]];
params.append(key, value);
}
});
const response = await fetch(`/api/users?${params}`);
return await response.json();
});
const users = await api.where('status', 'active');
GraphQL Builder¶
const gql = new AsyncCollection(async ({ operations }) => {
const filters = operations
.filter(([m]) => m === 'where')
.map(([_, key, value]) => `${key}: "${value}"`);
const query = `
query {
users(where: { ${filters.join(', ')} }) {
id name email
}
}
`;
const response = await graphqlClient.query(query);
return response.data.users;
});
Tips¶
- Keep executors focused - One responsibility per AsyncCollection
- Validate operations - Reject unsupported operations early
- Handle edge cases - Check for terminal operations like first/last
- Type everything - Use TypeScript generics for type safety
- Test thoroughly - Unit test your executor with different operation chains