Repository API
Complete reference for the Repository class methods
find()
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.
const users = await userRepo.find().execute(orm.client);
// Filtering — NOT find({ where })const active = await userRepo.findBy({ isActive: true });findOne()
async findOne(id: number | string, options?: { relations?: string[] }, client?: DBClient): Promise<T | null>Finds a single record by primary key.
const user = await userRepo.findOne(user.id);findOneBy() / findBy()
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.
const user = await userRepo.findOneBy({ email: "alice@example.com" });const admins = await userRepo.findBy({ role: "admin" }, { limit: 10 });findOrFail() / firstOrFail()
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.
const user = await userRepo.findOrFail(id); // never nullconst admin = await userRepo.firstOrFail({ role: "admin" });validateAll()
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.
const errors = userRepo.validateAll({ email: "nope", name: "ab" });// ["Field email does not match pattern", "Field name too short"]attach() / detach() / sync()
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.
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()
async create( entity: Partial<T>, options: { relations?: string[] } = {}, _client?: DBClient,): Promise<T>Creates a new record. Runs beforeCreate/afterCreate/beforeSave/afterSave hooks.
const user = await userRepo.create({ id: generateUUID(), email: "alice@example.com", name: "Alice" });bulkCreate()
async bulkCreate( entities: Partial<T>[], options: { relations?: string[]; batchSize?: number } = {}, _client?: DBClient,): Promise<T[]>Creates multiple records in batches.
await userRepo.bulkCreate([ { id: generateUUID(), name: "Alice", email: "alice@example.com" }, { id: generateUUID(), name: "Bob", email: "bob@example.com" },], { batchSize: 1000 });update()
async update( id: number | string, entity: Partial<T>, _client?: DBClient,): Promise<T>Updates a record by ID. Supports optimistic locking if configured.
const updated = await userRepo.update(user.id, { name: "Alice Smith" });upsert() / bulkUpsert()
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.
await userRepo.upsert({ email: "alice@example.com", name: "Alice" }, ["email"]);delete()
async delete(id: number | string, _client?: DBClient): Promise<void>Deletes a record. Soft delete if deletedAt column exists.
await userRepo.delete(user.id);recover() / recoverAll()
async recover(id: number | string, _client?: DBClient): Promise<T>async recoverAll(_client?: DBClient): Promise<number>Restores soft-deleted records.
await userRepo.recover(user.id);const count = await userRepo.recoverAll();count() / exists()
async count(conditions?: Partial<T>): Promise<number>async exists(conditions?: Partial<T>): Promise<boolean>const total = await userRepo.count();const activeCount = await userRepo.count({ isActive: true });const exists = await userRepo.exists({ email: "alice@example.com" });findAndCountAll()
async findAndCountAll(options?: { page?: number; pageSize?: number; conditions?: Partial<T>;}): Promise<{ data: T[]; total: number }>// Fetch page 1 with 20 items and total countconst { data, total } = await userRepo.findAndCountAll({ page: 1, pageSize: 20, conditions: { isActive: true },});console.log(`Showing ${data.length} of ${total} users`);toJSON()
toJSON(entity: T): Record<string, any>const user = await userRepo.findOne(id);const clean = userRepo.toJSON(user);// Excludes soft delete field, returns plain objectaggregate()
async aggregate(options: { count?: string | string[]; sum?: string[]; avg?: string[]; min?: string[]; max?: string[] }): Promise<Record<string, any>>const stats = await userRepo.aggregate({ count: "*", avg: ["age"], min: ["age"], max: ["age"] });Versioning Methods
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.
// Get all versionsconst history = await userRepo.history(user.id);
// Time-travel queryconst pastUser = await userRepo.asOf(user.id, new Date("2025-01-01"));
// Rollback to a versionawait userRepo.rollback(user.id, 2);Single-Row Helpers
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.
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
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.
await userRepo.updateBy({ isActive: false }, { role: "inactive" });await userRepo.deleteBy({ role: "spam" });await userRepo.restoreBy({ role: "spam" });await userRepo.truncate();Column & Aggregate Helpers
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): booleangetTableName(): stringgetSoftDeleteField(): string | nullgetIsVersioned(): booleanincrement(), 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.
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
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.
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
findDeleted(): QueryBuilder<T> // only soft-deleted rowswithTrashed(): QueryBuilder<T> // soft-deleted rows includedBoth 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.
const trashed = await userRepo.findDeleted().execute(orm.client);const all = await userRepo.withTrashed().execute(orm.client);Pagination & Rows
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.
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
find(): QueryBuilder<T>scope(name: string, ...args: any[]): QueryBuilder<T>scope() is find() with a model scope applied. Both return a builder.
const admins = await userRepo.scope("byRole", "admin").execute(orm.client);