Repository API

Complete reference for the Repository class methods

find()

typescript
find(): QueryBuilder<T>

Creates a QueryBuilder. Automatically excludes soft-deleted records when the model has a soft-delete column.

find() takes no arguments. It is not a finder — it returns the builder, and the builder is executed by .execute(client). Passing an options object does nothing: a call like userRepo.find({ where: { isActive: true } }) compiles to a plain builder and the filter is silently ignored, returning every row. Use findBy() for filtering.

typescript
const users = await userRepo.find().execute(orm.client);
// Filtering — NOT find({ where })
const active = await userRepo.findBy({ isActive: true });

findOne()

typescript
async findOne(id: number | string, options?: { relations?: string[] }, client?: DBClient): Promise<T | null>

Finds a single record by primary key.

typescript
const user = await userRepo.findOne(user.id);

findOneBy() / findBy()

typescript
async findOneBy(
conditions: Partial<T>,
options: { relations?: string[] } = {},
_client?: DBClient,
): Promise<T | null>
async findBy(
conditions: Partial<T>,
options: { relations?: string[]; limit?: number; orderBy?: string } = {},
_client?: DBClient,
): Promise<T[]>

TypeORM-style conditional finders. A condition whose value is null compiles to IS NULL. Both return the model's rows with options.relations eager-loaded.

typescript
const user = await userRepo.findOneBy({ email: "alice@example.com" });
const admins = await userRepo.findBy({ role: "admin" }, { limit: 10 });

findOrFail() / firstOrFail()

typescript
async findOrFail(
id: number | string,
options: { relations?: string[] } = {},
_client?: DBClient,
): Promise<T>
async firstOrFail(
conditions: Partial<T> = {},
options: { relations?: string[] } = {},
_client?: DBClient,
): Promise<T>

As findOne() and first(), but throws a StabilizeError with code NOT_FOUND_ERROR instead of returning null.

typescript
const user = await userRepo.findOrFail(id); // never null
const admin = await userRepo.firstOrFail({ role: "admin" });

validateAll()

typescript
validateAll(entity: Partial<T>, skipRequired = false): string[]

Returns every validation failure rather than throwing on the first, which is what a write path does. Empty when valid.

typescript
const errors = userRepo.validateAll({ email: "nope", name: "ab" });
// ["Field email does not match pattern", "Field name too short"]

attach() / detach() / sync()

typescript
async attach(
id: number | string,
relation: string,
targetIds: number | string | (number | string)[],
_client?: DBClient,
): Promise<number>
async detach(
id: number | string,
relation: string,
targetIds?: number | string | (number | string)[],
_client?: DBClient,
): Promise<number>
async sync(
id: number | string,
relation: string,
targetIds: (number | string)[],
_client?: DBClient,
): Promise<{ attached: number; detached: number }>

Edits a ManyToMany relation's join table directly. Idempotent, and accept a single id or an array. Omitting the ids from detach() unlinks everything. sync() makes the link set exactly the given ids and runs in a transaction.

typescript
await userRepo.attach(userId, "roles", [1, 2]);
await userRepo.detach(userId, "roles", [2]);
await userRepo.sync(userId, "roles", [3, 4]);
// { attached: 2, detached: 1 }

create()

typescript
async create(
entity: Partial<T>,
options: { relations?: string[] } = {},
_client?: DBClient,
): Promise<T>

Creates a new record. Runs beforeCreate/afterCreate/beforeSave/afterSave hooks.

typescript
const user = await userRepo.create({ id: generateUUID(), email: "alice@example.com", name: "Alice" });

bulkCreate()

typescript
async bulkCreate(
entities: Partial<T>[],
options: { relations?: string[]; batchSize?: number } = {},
_client?: DBClient,
): Promise<T[]>

Creates multiple records in batches.

typescript
await userRepo.bulkCreate([
{ id: generateUUID(), name: "Alice", email: "alice@example.com" },
{ id: generateUUID(), name: "Bob", email: "bob@example.com" },
], { batchSize: 1000 });

update()

typescript
async update(
id: number | string,
entity: Partial<T>,
_client?: DBClient,
): Promise<T>

Updates a record by ID. Supports optimistic locking if configured.

typescript
const updated = await userRepo.update(user.id, { name: "Alice Smith" });

upsert() / bulkUpsert()

typescript
async upsert(
entity: Partial<T>,
keys: string[],
_client?: DBClient,
): Promise<T>
async bulkUpsert(
entities: Partial<T>[],
keys: string[],
_client?: DBClient,
): Promise<T[]>

Insert or update based on unique keys.

typescript
await userRepo.upsert({ email: "alice@example.com", name: "Alice" }, ["email"]);

delete()

typescript
async delete(id: number | string, _client?: DBClient): Promise<void>

Deletes a record. Soft delete if deletedAt column exists.

typescript
await userRepo.delete(user.id);

recover() / recoverAll()

typescript
async recover(id: number | string, _client?: DBClient): Promise<T>
async recoverAll(_client?: DBClient): Promise<number>

Restores soft-deleted records.

typescript
await userRepo.recover(user.id);
const count = await userRepo.recoverAll();

count() / exists()

typescript
async count(conditions?: Partial<T>): Promise<number>
async exists(conditions?: Partial<T>): Promise<boolean>
typescript
const total = await userRepo.count();
const activeCount = await userRepo.count({ isActive: true });
const exists = await userRepo.exists({ email: "alice@example.com" });

findAndCountAll()

typescript
async findAndCountAll(options?: {
page?: number;
pageSize?: number;
conditions?: Partial<T>;
}): Promise<{ data: T[]; total: number }>
typescript
// Fetch page 1 with 20 items and total count
const { data, total } = await userRepo.findAndCountAll({
page: 1,
pageSize: 20,
conditions: { isActive: true },
});
console.log(`Showing ${data.length} of ${total} users`);

toJSON()

typescript
toJSON(entity: T): Record<string, any>
typescript
const user = await userRepo.findOne(id);
const clean = userRepo.toJSON(user);
// Excludes soft delete field, returns plain object

aggregate()

typescript
async aggregate(options: { count?: string | string[]; sum?: string[]; avg?: string[]; min?: string[]; max?: string[] }): Promise<Record<string, any>>
typescript
const stats = await userRepo.aggregate({ count: "*", avg: ["age"], min: ["age"], max: ["age"] });

Versioning Methods

typescript
async asOf(id: number | string, asOfDate: Date, _client?: DBClient): Promise<T | null>
async history(id: number | string, _client?: DBClient): Promise<T[]>
async rollback(id: number | string, version: number, _client?: DBClient): Promise<T>

All three require versioned: true on the model and throw a StabilizeError with code VERSIONING_ERROR otherwise. rollback() throws ROLLBACK_ERROR when the version does not exist.

typescript
// Get all versions
const history = await userRepo.history(user.id);
// Time-travel query
const pastUser = await userRepo.asOf(user.id, new Date("2025-01-01"));
// Rollback to a version
await userRepo.rollback(user.id, 2);

Single-Row Helpers

typescript
async first(conditions?: Partial<T>): Promise<T | null>
async last(field: string = "id"): Promise<T | null>
async random(): Promise<T | null>
async firstOrCreate(conditions: Partial<T>, defaults: Partial<T> = {}, _client?: DBClient): Promise<T>
async updateOrCreate(conditions: Partial<T>, updates: Partial<T>, _client?: DBClient): Promise<T>
async lockForUpdate(id: number | string, _client?: DBClient): Promise<T | null>

random() orders by the dialect's random function (RAND() on MySQL, NEWID() on SQL Server, RANDOM() elsewhere). lockForUpdate() emits FOR UPDATE on Postgres and MySQL only — SQLite and SQL Server have no equivalent clause, so it degrades to an ordinary read there.

typescript
const first = await userRepo.first({ role: "admin" });
const latest = await userRepo.last("createdAt");
const lucky = await userRepo.random();
const user = await userRepo.firstOrCreate({ email }, { name: "New" });
const updated = await userRepo.updateOrCreate({ email }, { name: "Renamed" });

Conditional Writes

typescript
async bulkUpdate(
updates: { where: { condition: string; params: any[] }; set: Partial<T> }[],
options: { batchSize?: number } = {},
_client?: DBClient,
): Promise<void>
async updateBy(conditions: Partial<T>, updates: Partial<T>, _client?: DBClient): Promise<number>
async deleteBy(conditions: Partial<T>, _client?: DBClient): Promise<number>
async restoreBy(conditions: Partial<T>, _client?: DBClient): Promise<number>
async truncate(_client?: DBClient): Promise<void>

updateBy() and deleteBy() throw a StabilizeError with code UNSAFE_QUERY when given an empty condition object, rather than affecting every row. truncate() is the explicit way to clear a table.
deleteBy() soft-deletes instead of deleting when the model has a soft-delete column.

typescript
await userRepo.updateBy({ isActive: false }, { role: "inactive" });
await userRepo.deleteBy({ role: "spam" });
await userRepo.restoreBy({ role: "spam" });
await userRepo.truncate();

Column & Aggregate Helpers

typescript
async pluck<K extends keyof T>(column: K): Promise<any[]>
async selectColumns(...columns: (keyof T)[]): Promise<Partial<T>[]>
async countDistinct(column: string): Promise<number>
async increment(id: number | string, field: string, amount: number = 1, _client?: DBClient): Promise<T>
async decrement(id: number | string, field: string, amount: number = 1, _client?: DBClient): Promise<T>
async toggle(id: number | string, field: string, _client?: DBClient): Promise<T>
hasColumn(field: string): boolean
getTableName(): string
getSoftDeleteField(): string | null
getIsVersioned(): boolean

increment(), decrement() and toggle() run a bare SQL UPDATE and then re-read the row, so they return the updated entity. toggle() flips a boolean, and on SQLite/SQL Server emits a CASE WHEN rather than NOT.

typescript
const emails = await userRepo.pluck("email");
const partial = await userRepo.selectColumns("id", "email");
const distinct = await userRepo.countDistinct("role");
await userRepo.increment(user.id, "loginCount", 1);
await userRepo.decrement(user.id, "credits", 5);
await userRepo.toggle(user.id, "isActive");

Bulk & Iteration

typescript
async bulkDelete(ids: (number | string)[], options: { batchSize?: number } = {}, _client?: DBClient): Promise<void>
async upsertMany(entities: Partial<T>[], keys: string[], batchSize: number = 100, _client?: DBClient): Promise<T[]>
async map<R>(query: QueryBuilder<T>, transform: (item: T) => R): Promise<R[]>
async each(query: QueryBuilder<T>, callback: (item: T, index: number) => void | Promise<void>, pageSize: number = 100): Promise<void>
async eachBatch(query: QueryBuilder<T>, callback: (batch: T[]) => void | Promise<void>, batchSize: number = 100): Promise<void>

bulkDelete() runs the per-row hooks for each id it finds and skips ids that do not exist. each() and eachBatch() page through the query with LIMIT/OFFSET until a short page comes back — they do not run in a transaction, so a concurrent write can shift rows between pages.

typescript
await userRepo.bulkDelete([1, 2, 3], { batchSize: 500 });
const names = await userRepo.map(
userRepo.find(),
(user) => user.name,
);
await userRepo.each(userRepo.find(), async (user, index) => {
console.log(index, user.email);
});
await userRepo.eachBatch(userRepo.find(), async (batch) => {
await externalApi.send(batch);
});

Soft-Delete Queries

typescript
findDeleted(): QueryBuilder<T> // only soft-deleted rows
withTrashed(): QueryBuilder<T> // soft-deleted rows included

Both return a builder, so they are executed with .execute(client). findDeleted() throws a StabilizeError with code QUERY_ERROR when the model has no soft-delete column.

typescript
const trashed = await userRepo.findDeleted().execute(orm.client);
const all = await userRepo.withTrashed().execute(orm.client);

Pagination & Rows

typescript
async paginate(
page: number,
pageSize: number,
options: any = {},
): Promise<{ data: T[]; total: number; page: number; pageSize: number }>
async findAndCount(options: { relations?: string[] } = {}): Promise<{ data: T[]; total: number }>
async findMany(options: {
where?: Partial<T>;
cursor?: { field: string; value: any; direction?: "forward" | "backward" };
take?: number;
skip?: number;
orderBy?: { field: string; direction: "ASC" | "DESC" };
relations?: string[];
} = {}): Promise<T[]>
async seed(data: Partial<T>[], options: { ignoreDuplicates?: boolean } = {}, _client?: DBClient): Promise<T[]>
async healthCheck(): Promise<{ status: string; table: string; rows: number; latencyMs: number }>
async rawQuery<R = T>(query: string, params?: any[]): Promise<R[]>

paginate() accepts an options argument but the current implementation does not read it — filtering is not applied to either the page or the total. Use findAndCountAll() when you need conditions. findMany() is the cursor-based reader; see the Pagination page.

typescript
const page = await userRepo.paginate(1, 20);
// { data: [...], total: 100, page: 1, pageSize: 20 }
const { data, total } = await userRepo.findAndCount({ relations: ["posts"] });
const health = await userRepo.healthCheck();
// { status: "healthy", table: "users", rows: 42, latencyMs: 0.5 }
await userRepo.seed([{ id: generateUUID(), name: "Admin" }], { ignoreDuplicates: true });

Reusable Scopes

typescript
find(): QueryBuilder<T>
scope(name: string, ...args: any[]): QueryBuilder<T>

scope() is find() with a model scope applied. Both return a builder.

typescript
const admins = await userRepo.scope("byRole", "admin").execute(orm.client);