Collections
Introduction
All multi-row results returned by Orion — from Model.get(), relation queries, and Model.all() — are instances of ModelCollection<T>. This class extends the base Collection<T> with model-aware methods for PK lookups, eager loading, and batch database operations.
Collection<T>
Collection<T> is a typed wrapper around a plain array. It is the base class for ModelCollection<T> and is also returned by some raw query operations.
Available Methods
// Inspection
col.all() // T[]
col.count() // number
col.isEmpty() // boolean
col.isNotEmpty() // boolean
col.contains(item) // boolean (by value or predicate)
col.first() // T | undefined
col.first(fn) // T | undefined — first matching predicate
col.last() // T | undefined
col.last(fn) // T | undefined
// Retrieval
col.find(fn) // T | undefined
col.get(index) // T | undefined
// Transformation
col.map(fn) // Collection<U>
col.flatMap(fn) // Collection<U>
col.filter(fn) // Collection<T>
col.reject(fn) // Collection<T> — inverse of filter
col.each(fn) // void — iterate, returning this for chaining
col.reduce(fn, init) // accumulator result
col.pluck(key) // Collection<V> — values for a given property
col.groupBy(key) // Record<string, Collection<T>>
col.keyBy(key) // Record<string, T>
col.chunk(size) // Collection<Collection<T>>
col.flatten() // Collection<unknown>
col.unique(key?) // Collection<T>
col.reverse() // Collection<T>
col.sort(compareFn?) // Collection<T>
col.sortBy(key) // Collection<T>
col.sortByDesc(key) // Collection<T>
// Merging
col.push(...items) // Collection<T>
col.merge(other) // Collection<T>
col.concat(other) // Collection<T>
col.prepend(item) // Collection<T>
col.append(item) // Collection<T> — add to end (non-model version)
// Slicing
col.slice(start, end?) // Collection<T>
col.take(n) // Collection<T> — first N items
col.skip(n) // Collection<T> — skip first N
col.forPage(page, perPage) // Collection<T>
// Aggregates
col.sum(key) // number
col.avg(key) // number
col.min(key) // number
col.max(key) // number
// Serialization
col.toArray() // T[]
col.toJSON() // stringModelCollection<T>
ModelCollection<T extends Model> is returned by all ModelBuilder.get() calls and most relation queries. It adds model-specific methods on top of Collection<T>.
PK-Based Methods
// Find by primary key — returns the model or undefined
const user = users.findByKey(1);
// Find or throw — throws ModelNotFoundException if missing
const user = users.findOrFail(1);
// All primary keys as an array
const ids = users.modelKeys(); // [1, 2, 3, 4, 5]
// Exclude by PK
const others = users.except([1, 2]);
// Include only by PK
const selected = users.only([3, 4]);
// Models present in this collection but NOT in another (compared by PK)
const diff = users.diff(otherUsers);
// Models present in BOTH collections (compared by PK)
const common = users.intersect(activeUsers);
// Check if a PK or model instance is in the collection
users.contains(1);
users.contains(someUser);
// Deduplicate by PK
const unique = users.unique();
// Split into two groups — [matching, notMatching]
const [admins, regular] = users.partition((u) => u._attributes.role === 'admin');Database Operations
// Re-fetch all models from the database — returns a new ModelCollection
const freshUsers = await users.fresh();
// Re-fetch with eager-loaded relationships
const freshUsers = await users.fresh('posts');
const freshUsers = await users.fresh(['posts', 'roles']);
// Eager-load a relationship onto the existing instances (in-place)
await users.load('posts');
await users.load(['posts', 'roles']);
await users.load({ posts: (q) => q.where('published', true) });
// Only load if not already loaded
await users.loadMissing('posts');
await users.loadMissing(['posts', 'roles']);
// Build a ModelBuilder with WHERE id IN (...) for these models
const builder = users.toQuery();
await users.toQuery().update({ status: 'archived' });Aggregate Loading
await users.loadCount('posts');
await users.loadCount({ posts: (q) => q.where('published', true) });
await users.loadSum('orders', 'amount');
await users.loadMin('orders', 'amount');
await users.loadMax('orders', 'amount');
await users.loadAvg('reviews', 'rating');
await users.loadExists('orders');
// Access the loaded value on each model
for (const user of users) {
user.getRelation<number>('posts_count');
user.getRelation<number>('orders_sum_amount');
}Serialization Helpers
These methods delegate to each model in the collection and return a new ModelCollection with the overrides applied:
// Visibility
users.makeVisible(['phone']);
users.makeHidden(['email', 'password']);
users.setVisible(['id', 'name', 'email']); // replace entire visible list
users.setHidden(['password', 'token']); // replace entire hidden list
users.mergeVisible(['phone']); // add to current visible list
users.mergeHidden(['secret']); // add to current hidden list
// Appended accessors
users.append('full_name');
users.append(['full_name', 'avatar_url']);
users.setAppends(['full_name']);
users.withoutAppends();
// Resource conversion (requires @UseResourceCollection on the model)
users.toResourceCollection();Custom Collections
You can bind a custom collection class to a model using the @CollectedBy decorator or by overriding newCollection(). Every call that would normally return ModelCollection<User> will return your custom class instead.
With decorator:
import { ModelCollection } from '@wrsouza/orion';
class UserCollection extends ModelCollection<User> {
admins(): UserCollection {
return this.filter((u) => u._attributes.role === 'admin') as UserCollection;
}
suspend(): Promise<void>[] {
return this.map((u) => u.update({ suspended: true })).all();
}
}import { CollectedBy } from '@wrsouza/orion'; // planned — see gap audit
@CollectedBy(UserCollection)
@table('users')
class User extends Model {}
const users = await User.all(); // UserCollection<User>
users.admins().count();With newCollection() override:
@table('users')
class User extends Model {
static newCollection(models: User[] = []): UserCollection {
return new UserCollection(models);
}
}