Advanced Patterns¶
Advanced patterns and real-world techniques for using @arcaelas/collection in production applications.
Complex Queries¶
Multi-Level Nested Queries¶
Combine multiple operators and nested conditions for complex data filtering:
interface User {
id: number;
name: string;
profile: {
age: number;
verified: boolean;
subscription: {
tier: string;
expiresAt: Date;
};
};
permissions: string[];
metadata: Record<string, any>;
}
const users = new Collection<User>([...]);
// Find premium users with specific criteria
const eligibleUsers = users.filter({
// Profile validations
'profile.verified': true,
'profile.age': { $gte: 18, $lte: 65 },
// Subscription checks
'profile.subscription.tier': { $in: ['premium', 'enterprise'] },
'profile.subscription.expiresAt': { $gt: Date.now() },
// Permission requirements
permissions: {
$contains: 'access:dashboard',
$not: { $contains: 'restricted' }
},
// Metadata filtering
'metadata.region': { $regex: /^(US|EU|UK)$/ },
'metadata.lastLogin': { $exists: true }
});
Dynamic Query Building¶
Build queries programmatically based on runtime conditions:
function buildUserQuery(filters: {
age?: { min?: number; max?: number };
verified?: boolean;
roles?: string[];
search?: string;
}) {
const query: any = {};
if (filters.age) {
query['profile.age'] = {};
if (filters.age.min !== undefined) {
query['profile.age'].$gte = filters.age.min;
}
if (filters.age.max !== undefined) {
query['profile.age'].$lte = filters.age.max;
}
}
if (filters.verified !== undefined) {
query['profile.verified'] = filters.verified;
}
if (filters.roles && filters.roles.length > 0) {
query.roles = { $in: filters.roles };
}
if (filters.search) {
query.$or = [
{ name: { $regex: new RegExp(filters.search, 'i') } },
{ email: { $regex: new RegExp(filters.search, 'i') } }
];
}
return query;
}
// Usage
const query = buildUserQuery({
age: { min: 21, max: 65 },
verified: true,
roles: ['admin', 'moderator'],
search: 'john'
});
const results = users.filter(query);
Computed Filters¶
Combine queries with computed functions for complex business logic:
const activeOrders = orders.filter(order => {
// Complex date logic
const daysSinceOrder = (Date.now() - order.createdAt.getTime()) / (1000 * 60 * 60 * 24);
const isRecent = daysSinceOrder <= 30;
// Complex calculation
const totalWithTax = order.items.reduce((sum, item) =>
sum + (item.price * item.quantity * (1 + item.taxRate)), 0
);
const isSignificant = totalWithTax >= 1000;
// Business rules
const hasValidStatus = ['pending', 'processing', 'shipped'].includes(order.status);
const customerTier = order.customer.tier;
return isRecent && isSignificant && hasValidStatus &&
(customerTier === 'premium' || totalWithTax >= 2000);
});
Custom Macros¶
Repository Pattern¶
Create a custom repository with domain-specific methods:
class UserRepository extends Collection<User> {
constructor(users: User[]) {
super(users);
// Add domain-specific macros
this.macro('active', function() {
return this.filter({ active: true, deletedAt: { $exists: false } });
});
this.macro('admins', function() {
return this.filter({ roles: { $contains: 'admin' } });
});
this.macro('verified', function() {
return this.filter({ emailVerified: true, phoneVerified: true });
});
this.macro('byTier', function(tier: string) {
return this.filter({ 'subscription.tier': tier });
});
this.macro('recentlyActive', function(days: number = 7) {
const threshold = Date.now() - (days * 24 * 60 * 60 * 1000);
return this.filter({ lastActivityAt: { $gte: threshold } });
});
}
// Additional methods
findByEmail(email: string): User | undefined {
return this.first({ email });
}
findById(id: number): User | undefined {
return this.first({ id });
}
getTierDistribution() {
return this.countBy('subscription.tier');
}
}
// Usage
const userRepo = new UserRepository(await db.users.findAll());
const premiumActiveUsers = userRepo
.active()
.verified()
.byTier('premium')
.recentlyActive(30);
const admin = userRepo.findByEmail('admin@example.com');
Query Builder Macro¶
Create a fluent query builder using macros:
Collection.macro('query', function() {
return new QueryBuilder(this);
});
class QueryBuilder<T> {
private collection: Collection<T>;
private conditions: any[] = [];
constructor(collection: Collection<T>) {
this.collection = collection;
}
where(field: string, operator: string, value: any) {
this.conditions.push({ field, operator, value });
return this;
}
orWhere(field: string, operator: string, value: any) {
// Implementation for OR conditions
return this;
}
orderBy(field: string, direction: 'asc' | 'desc' = 'asc') {
this.collection = this.collection.sort(field, direction);
return this;
}
limit(count: number) {
this.collection = this.collection.slice(0, count) as Collection<T>;
return this;
}
execute(): Collection<T> {
// Build final query from conditions
const query = this.conditions.reduce((acc, cond) => {
acc[cond.field] = { [`$${cond.operator}`]: cond.value };
return acc;
}, {});
return this.collection.filter(query);
}
}
// Usage
const results = users
.query()
.where('age', 'gte', 18)
.where('status', 'eq', 'active')
.orderBy('created_at', 'desc')
.limit(50)
.execute();
Performance Optimization¶
Lazy Evaluation¶
Defer expensive operations until results are needed:
class LazyCollection<T> {
private data: T[];
private operations: Array<(data: T[]) => T[]> = [];
constructor(data: T[]) {
this.data = data;
}
filter(predicate: (item: T) => boolean) {
this.operations.push(data => data.filter(predicate));
return this;
}
map<U>(mapper: (item: T) => U): LazyCollection<U> {
this.operations.push(data => data.map(mapper) as any);
return this as any;
}
take(count: number) {
this.operations.push(data => data.slice(0, count));
return this;
}
execute(): T[] {
return this.operations.reduce((data, op) => op(data), this.data);
}
}
// Execute only when needed
const result = new LazyCollection(largeDataset)
.filter(x => x.active)
.map(x => ({ ...x, computed: heavyComputation(x) }))
.take(10)
.execute(); // Only processes first 10 active items
Memoization¶
Cache expensive query results:
class MemoizedCollection<T> extends Collection<T> {
private cache = new Map<string, any>();
filter(query: any): Collection<T> {
const key = JSON.stringify(query);
if (this.cache.has(key)) {
return this.cache.get(key);
}
const result = super.filter(query);
this.cache.set(key, result);
return result;
}
clearCache() {
this.cache.clear();
}
}
// Subsequent calls use cached results
const users = new MemoizedCollection(data);
users.filter({ active: true }); // Computed
users.filter({ active: true }); // Cached
Batch Processing¶
Process large datasets in chunks to avoid memory issues:
async function processBatched<T, R>(
collection: Collection<T>,
batchSize: number,
processor: (batch: T[]) => Promise<R>
): Promise<R[]> {
const chunks = collection.chunk(batchSize);
const results: R[] = [];
for (const chunk of chunks) {
const result = await processor(chunk);
results.push(result);
// Optional: Yield to event loop
await new Promise(resolve => setImmediate(resolve));
}
return results;
}
// Process 1M records in batches
const users = new Collection(millionUsers);
await processBatched(users, 1000, async (batch) => {
await sendEmails(batch);
return { sent: batch.length };
});
Index-Based Lookups¶
Create indices for fast lookups on large collections:
class IndexedCollection<T> extends Collection<T> {
private indices = new Map<string, Map<any, T[]>>();
createIndex(key: string) {
const index = new Map<any, T[]>();
this.forEach(item => {
const value = (item as any)[key];
if (!index.has(value)) {
index.set(value, []);
}
index.get(value)!.push(item);
});
this.indices.set(key, index);
}
findByIndex(key: string, value: any): T[] {
const index = this.indices.get(key);
if (!index) {
throw new Error(`Index '${key}' not found`);
}
return index.get(value) || [];
}
}
// O(1) lookups instead of O(n)
const users = new IndexedCollection(millionUsers);
users.createIndex('email');
users.createIndex('country');
const user = users.findByIndex('email', 'john@example.com')[0]; // Instant
const usUsers = users.findByIndex('country', 'US'); // Instant
Integration Patterns¶
Prisma Integration¶
Use AsyncCollection with Prisma for deferred query execution:
import { PrismaClient } from '@prisma/client';
import AsyncCollection from '@arcaelas/collection/async';
const prisma = new PrismaClient();
const users = new AsyncCollection(async ({ operations }) => {
const where: any = {};
const orderBy: any[] = [];
let take: number | undefined;
let skip: number | undefined;
operations.forEach(([method, ...args]) => {
switch (method) {
case 'where':
const [key, op, val] = args.length === 3 ? args : [args[0], '=', args[1]];
if (op === '>=') {
where[key] = { gte: val };
} else if (op === '>') {
where[key] = { gt: val };
} else {
where[key] = val;
}
break;
case 'sort':
orderBy.push({ [args[0]]: args[1] || 'asc' });
break;
case 'paginate':
const [page, perPage] = args;
skip = (page - 1) * perPage;
take = perPage;
break;
}
});
return await prisma.user.findMany({ where, orderBy, take, skip });
});
// Builds and executes Prisma query
const result = await users
.where('age', '>=', 18)
.where('verified', true)
.sort('created_at', 'desc')
.paginate(1, 20);
GraphQL Resolver¶
Use Collection to process GraphQL results:
const resolvers = {
Query: {
users: async (_: any, args: any, context: any) => {
const allUsers = await context.db.users.findAll();
const users = new Collection(allUsers);
let result = users;
if (args.filter) {
if (args.filter.role) {
result = result.filter({ role: args.filter.role });
}
if (args.filter.verified !== undefined) {
result = result.filter({ verified: args.filter.verified });
}
if (args.filter.search) {
result = result.filter(user =>
user.name.toLowerCase().includes(args.filter.search.toLowerCase())
);
}
}
if (args.orderBy) {
const [field, direction] = args.orderBy.split('_');
result = result.sort(field, direction.toLowerCase());
}
if (args.pagination) {
const { page, perPage } = args.pagination;
const paginated = result.paginate(page, perPage);
return {
items: paginated.items,
pageInfo: {
currentPage: page,
hasNextPage: !!paginated.next,
hasPreviousPage: !!paginated.prev
}
};
}
return { items: result, pageInfo: { currentPage: 1, hasNextPage: false, hasPreviousPage: false } };
}
}
};
REST API Middleware¶
Create Express middleware for collection-based filtering:
import express from 'express';
function collectionMiddleware<T>(
dataFetcher: () => Promise<T[]>
) {
return async (req: express.Request, res: express.Response) => {
const data = await dataFetcher();
let collection = new Collection(data);
// Apply filters from query params
if (req.query.filter) {
const filters = JSON.parse(req.query.filter as string);
collection = collection.filter(filters);
}
// Apply sorting
if (req.query.sort) {
const [field, direction] = (req.query.sort as string).split(':');
collection = collection.sort(field, direction as 'asc' | 'desc');
}
// Apply pagination
const page = parseInt(req.query.page as string) || 1;
const perPage = parseInt(req.query.perPage as string) || 20;
const paginated = collection.paginate(page, perPage);
res.json({
data: paginated.items,
meta: {
total: collection.length,
page,
perPage,
hasMore: !!paginated.next
}
});
};
}
// Usage
app.get('/api/users', collectionMiddleware(async () =>
await db.users.findAll()
));
Data Processing Pipelines¶
ETL Pipeline¶
Extract, transform, and load data using Collection:
async function etlPipeline() {
// Extract
const rawData = await fetchFromApi();
// Transform
const processed = new Collection(rawData)
// Validation
.filter(item => item.id && item.name && item.email)
// Normalize
.map(item => ({
...item,
email: item.email.toLowerCase().trim(),
name: item.name.trim(),
createdAt: new Date(item.createdAt)
}))
// Deduplicate
.unique('email')
// Enrich
.map(item => ({
...item,
domain: item.email.split('@')[1],
accountAge: Date.now() - item.createdAt.getTime()
}))
// Filter
.filter({ active: true })
.filter(item => item.accountAge > 0)
// Sort
.sort('createdAt', 'desc');
// Load
await Promise.all(
processed.chunk(100).map(batch =>
db.users.insertMany(batch)
)
);
return {
total: rawData.length,
processed: processed.length,
rejected: rawData.length - processed.length
};
}
Stream Processing¶
Process data streams with Collection:
import { Readable } from 'stream';
class CollectionStream<T> extends Readable {
private collection: Collection<T>;
private index = 0;
constructor(collection: Collection<T>) {
super({ objectMode: true });
this.collection = collection;
}
_read() {
if (this.index < this.collection.length) {
this.push(this.collection[this.index]);
this.index++;
} else {
this.push(null); // End stream
}
}
}
// Usage
const users = new Collection([...]);
const stream = new CollectionStream(users.filter({ active: true }));
stream
.on('data', async (user) => {
await processUser(user);
})
.on('end', () => {
console.log('Processing complete');
});
State Management¶
React State Management¶
Use Collection for complex state operations:
import { useState, useCallback } from 'react';
function useCollectionState<T>(initial: T[]) {
const [data, setData] = useState(() => new Collection(initial));
const filter = useCallback((query: any) => {
setData(prev => prev.filter(query));
}, []);
const add = useCallback((item: T) => {
setData(prev => new Collection([...prev, item]));
}, []);
const update = useCallback((where: any, values: Partial<T>) => {
setData(prev => {
const updated = prev.collect();
updated.update(where, values);
return updated;
});
}, []);
const remove = useCallback((where: any) => {
setData(prev => {
const updated = prev.collect();
updated.delete(where);
return updated;
});
}, []);
return { data, filter, add, update, remove };
}
// Usage in component
function UserList() {
const { data, filter, add, update, remove } = useCollectionState(users);
return (
<div>
<button onClick={() => filter({ active: true })}>
Show Active
</button>
{data.map(user => (
<UserCard
key={user.id}
user={user}
onUpdate={(values) => update({ id: user.id }, values)}
onDelete={() => remove({ id: user.id })}
/>
))}
</div>
);
}
Testing Patterns¶
Test Fixtures¶
Create reusable test data with Collection:
class TestDataFactory {
static users(count: number = 10) {
return new Collection(
Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@test.com`,
active: i % 2 === 0,
role: i % 3 === 0 ? 'admin' : 'user'
}))
);
}
static orders(userId: number, count: number = 5) {
return new Collection(
Array.from({ length: count }, (_, i) => ({
id: i + 1,
userId,
total: Math.random() * 1000,
status: ['pending', 'completed', 'cancelled'][i % 3],
createdAt: new Date(Date.now() - i * 86400000)
}))
);
}
}
// Usage in tests
describe('UserService', () => {
it('should filter active users', () => {
const users = TestDataFactory.users(100);
const service = new UserService(users);
const active = service.getActiveUsers();
expect(active.length).toBe(50);
expect(active.every({ active: true })).toBe(true);
});
});
Mock AsyncCollection¶
Mock async data sources for testing:
class MockAsyncCollection<T> extends AsyncCollection<T> {
private mockData: T[];
constructor(data: T[]) {
super(async () => data);
this.mockData = data;
}
// Add test helpers
reset(data: T[]) {
this.mockData = data;
}
getMockData() {
return this.mockData;
}
}
// Usage
describe('API Integration', () => {
it('should fetch and filter users', async () => {
const mockUsers = new MockAsyncCollection([
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false }
]);
const result = await mockUsers.where('active', true);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
});
See Also¶
- Core Concepts - Fundamental principles
- Best Practices - Coding standards
- Performance Guide - Optimization techniques
- TypeScript Usage - Type safety patterns