Repositories
Repositories provide a dedicated abstraction layer for data access, promoting separation of concerns and cleaner code architecture.
Repository Pattern vs Active Record
In Carno ORM, repositories are a separate way to work with persistence.
Repositorykeeps query and write logic outside the entity class.BaseEntityis optional in this pattern.- A plain class decorated with
@Entity()is enough to use a repository. save(),remove(),isPersisted()and dirty tracking are not part of the Repository model unless you explicitly choose to also extendBaseEntity.
For most service-oriented applications, this is the recommended style.
Defining the Entity
For Repository-oriented code, the most explicit shape is a plain entity:
import { Entity, PrimaryKey, Property } from '@carno.js/orm';
@Entity()
export class User {
@PrimaryKey()
id: number;
@Property()
name: string;
@Property({ hidden: true })
internalNotes: string;
}
Even without BaseEntity, the decorated class still has ORM-aware serialization behavior.
Creating a Repository
To create a repository, extend the generic Repository<T> class and decorate it with @Service().
import { Service, Repository } from '@carno.js/orm';
import { User } from '../entities/user.entity';
@Service()
export class UserRepository extends Repository<User> {
constructor() {
super(User);
}
// Custom method to find users by role
async findByRole(role: string): Promise<User[]> {
return this.find({
where: { role }
});
}
}
What Repositories Do and Do Not Provide
Using a repository with a plain entity gives you:
create,find,findOne,findAllupdate,updateByIddelete,deleteByIdcount,exists- rich serialization based on
@Entity()metadata
It does not give you:
User.find()User.create()user.save()user.remove()- dirty tracking
If you need those behaviors, you are in Active Record territory and should extend BaseEntity.
Using Repositories
Inject the repository into your services or controllers.
@Service()
export class UserService {
constructor(private userRepository: UserRepository) {}
async getAllAdmins() {
return this.userRepository.findByRole('admin');
}
}
Serialization in Repository Mode
@Entity()-decorated classes returned by a repository still serialize using ORM metadata.
That means:
- hidden fields are excluded
- computed fields are included
- loaded relations serialize consistently
This behavior is specific to ORM entities. Plain classes without @Entity() do not participate in ORM-aware serialization.
Standard Methods
The Repository class comes with a comprehensive set of built-in methods for common operations.
Reading
find(options): Finds multiple entities matching the criteria.findOne(options): Finds a single entity. Returnsundefinedif not found.findOneOrFail(options): Finds a single entity. Throws an error if not found.findAll(options): Finds all entities (wrapper aroundfindwithout required where).findById(id): Finds an entity by its primary key.findByIdOrFail(id): Finds an entity by primary key or throws.exists(where): Checks if at least one entity matches the criteria.count(where): Returns the count of entities matching the criteria.
Writing
-
create(data): Creates and persists a new entity instance immediately.const user = await this.userRepository.create({
name: 'Alice',
email: 'alice@example.com'
}); -
update(where, data): Updates entities matching the criteria. See Querying & Operators for details on thewhereargument.await this.userRepository.update(
{ id: 1 },
{ name: 'Alice Smith' }
);Computed updates are also supported with
expr(...).import { expr } from '@carno.js/orm';
await this.userRepository.update(
{ id: 1 },
{ experience: expr((prev) => prev.plus(100)) }
);Use this when the new value depends on the current value already stored in the row and you want the database to do the calculation directly. The repository delegates this to the ORM query builder, so the operation is still executed as a single SQL
UPDATE.For the example above, the SQL generated is:
UPDATE "user" as u1
SET experience = experience + 100
WHERE (u1.id = 1)previs not a loaded entity value. It is an expression builder for the current SQL column value. Available arithmetic helpers areplus,minus,times, anddiv. -
updateById(id, data): Updates a specific entity by ID.
Deleting Data
You can delete records using a specific criteria or by ID.
-
delete(where): Deletes entities matching the criteria. See Querying & Operators for supported filters.// Delete all inactive users
await this.userRepository.delete({ isActive: false }); -
deleteById(id): Deletes a specific entity by ID.await this.userRepository.deleteById(5);
Query Builder Access
If the standard methods aren't enough, you can access the underlying Query Builder from within a repository.
async findRecentUsers() {
return this.createQueryBuilder()
.where({ createdAt: { $gt: new Date(Date.now() - 86400000) } })
.orderBy({ createdAt: 'DESC' })
.executeAndReturnAll();
}