alepha@docs:~/docs/guides/persistence$
cat 1-repository.md
6 min read
Last commit:

#Repository

Alepha ORM is built on top of Drizzle ORM and Drizzle Kit.

$entity defines a database table. $repository creates a type-safe data access layer for that table.

Alepha main target is PostgreSQL, but SQLite are also supported.

The API is mostly database-agnostic, but some features (e.g. certain column types or operators) may be database-specific.

typescript
1import { t } from "alepha";2import { $entity, $repository, db } from "alepha/orm";

#Defining an Entity

An entity maps directly to a database table. The schema uses Alepha's t type system combined with db helpers for database-specific column types.

typescript
 1import { t } from "alepha"; 2import { $entity, db } from "alepha/orm"; 3  4const product = $entity({ 5  name: "products", 6  schema: t.object({ 7    id: db.primaryKey(t.uuid()), 8    name: t.text(), 9    price: t.number(),10    createdAt: db.createdAt(),11    updatedAt: db.updatedAt(),12  }),13  indexes: [14    { column: "name", unique: true },15  ],16});

The name field sets the database table name. The schema field defines columns using TypeBox schemas. The indexes field configures database indexes for query optimization.

#Index Options

Indexes accept several forms:

typescript
1indexes: [2  "name",                                    // simple index on one column3  { column: "email", unique: true },         // unique index on one column4  { columns: ["tenantId", "name"], unique: true }, // composite unique index5  { column: "status", name: "idx_status" },  // index with custom name6],

#Constraints

Entities support unique constraints and check constraints at the table level:

typescript
 1const user = $entity({ 2  name: "users", 3  schema: t.object({ 4    id: db.primaryKey(t.uuid()), 5    tenantId: t.uuid(), 6    username: t.text(), 7    age: t.integer(), 8  }), 9  constraints: [10    { columns: ["tenantId", "username"], unique: true },11    { columns: ["age"], check: sql`age >= 0 AND age <= 150` },12  ],13});

#Foreign Keys

Explicit foreign key constraints can be declared at the entity level:

typescript
1foreignKeys: [2  {3    columns: ["authorId"],4    foreignColumns: [() => user.cols.id],5  },6],

For single-column foreign keys, prefer db.ref() on the column itself (see Special Columns).

#Creating a Repository

Use $repository as a class property to get a fully typed repository:

typescript
1class ProductService {2  repo = $repository(product);3}

Relations between tables are NOT handled by $entity. Instead, use the with option in repository query methods to perform joins.

#Query Methods

#findMany

Find multiple records. Supports where, limit, offset, orderBy, groupBy, distinct, columns, and with (joins).

typescript
1const items = await this.repo.findMany({2  where: { price: { gte: 10 } },3  orderBy: { column: "name", direction: "asc" },4  limit: 20,5  offset: 0,6});

#findOne

Find a single record. Returns undefined if not found.

typescript
1const item = await this.repo.findOne({2  where: { name: { eq: "Widget" } },3});

#getOne

Find a single record. Throws DbEntityNotFoundError if not found.

typescript
1const item = await this.repo.getOne({2  where: { name: { eq: "Widget" } },3});

#findById / getById

Look up a record by primary key. findById returns undefined if not found, getById throws DbEntityNotFoundError.

typescript
1const item = await this.repo.findById("some-uuid");2const item = await this.repo.getById("some-uuid"); // throws if missing

#paginate

Returns paginated results with metadata.

typescript
 1const page = await this.repo.paginate( 2  { page: 0, size: 10, sort: "name,asc" }, 3  { where: { price: { gt: 0 } } }, 4  { count: true }, 5); 6  7// page.data        -> T[] 8// page.page.size   -> number 9// page.page.totalElements -> number (when count: true)10// page.page.totalPages    -> number (when count: true)

#count

Count matching records.

typescript
1const total = await this.repo.count({ status: { eq: "active" } });

#query

Execute raw SQL using Drizzle's sql tagged template. Returns decoded entities.

typescript
1import { sql } from "alepha/orm";2 3const results = await this.repo.query(4  (table) => sql`SELECT * FROM ${table} WHERE ${table.price} > ${100}`,5);

#Create Methods

#create

Create a single entity. Returns the full created entity.

typescript
1const created = await this.repo.create({2  name: "Widget",3  price: 9.99,4});

#createMany

Batch-create entities. Inserts are batched in chunks of 1000 by default.

typescript
1const items = await this.repo.createMany(2  [{ name: "A", price: 1 }, { name: "B", price: 2 }],3  { batchSize: 500 },4);

#upsert

Insert a new entity or update an existing one if a conflict is detected. Works on both PostgreSQL and SQLite.

typescript
 1// Simple upsert on primary key 2const product = await this.repo.upsert({ 3  id: "some-uuid", 4  name: "Widget", 5  price: 9.99, 6}); 7  8// Upsert on a unique column 9const product = await this.repo.upsert(10  { id: "some-uuid", sku: "WIDGET-1", name: "Widget", price: 9.99 },11  { target: ["sku"] },12);13 14// Upsert with custom update fields (only update price on conflict)15const product = await this.repo.upsert(16  { id: "some-uuid", sku: "WIDGET-1", name: "Widget", price: 19.99 },17  { target: ["sku"], set: { price: 19.99 } },18);
  • target — column(s) to detect conflicts on. Defaults to the primary key.
  • set — fields to update on conflict. Defaults to the insert data minus the target and primary key columns.

If the entity has an updatedAt column, it is automatically set on conflict.

#Update Methods

#updateOne

Find a single entity by where clause and update it. Throws DbEntityNotFoundError if not found. Returns the updated entity.

typescript
1const updated = await this.repo.updateOne(2  { name: { eq: "Widget" } },3  { price: 12.99 },4);

#updateById

Update by primary key. Returns the updated entity.

typescript
1const updated = await this.repo.updateById("some-uuid", { price: 12.99 });

#updateMany

Update multiple records matching a where clause. Returns an array of updated entity IDs.

typescript
1const ids = await this.repo.updateMany(2  { status: { eq: "draft" } },3  { status: "published" },4);

#save

Save a previously fetched entity. Uses optimistic locking when a version column is present. Unlike updateOne/updateById, save expects the full entity object and sets undefined fields to null.

typescript
1const entity = await this.repo.findById("some-uuid");2entity.name = "Updated Name";3await this.repo.save(entity);

If the version has changed since the entity was fetched, save throws DbVersionMismatchError.

#Delete Methods

#deleteOne

Delete a single entity matching the where clause. Returns an array of deleted IDs.

typescript
1await this.repo.deleteOne({ name: { eq: "Widget" } });

#deleteById

Delete by primary key. Throws DbEntityNotFoundError if not found.

typescript
1await this.repo.deleteById("some-uuid");

#deleteMany

Delete multiple records matching a where clause. Returns an array of deleted IDs.

typescript
1const ids = await this.repo.deleteMany({ status: { eq: "archived" } });

#destroy

Delete a previously fetched entity by its primary key.

typescript
1const entity = await this.repo.findById("some-uuid");2await this.repo.destroy(entity);

#clear

Delete all records in the table.

typescript
1await this.repo.clear();

#Soft Delete

If the entity schema includes a db.deletedAt() column, all delete operations automatically perform a soft delete by setting the deletedAt timestamp instead of removing the row. All query operations automatically filter out soft-deleted records.

To perform a hard delete on a soft-deletable entity, pass { force: true }:

typescript
1await this.repo.deleteById("some-uuid", { force: true });

To include soft-deleted records in queries, also use { force: true }:

typescript
1const all = await this.repo.findMany({}, { force: true });

#Where Clause Operators

Where clauses accept either a direct value (shorthand for eq) or an object with filter operators:

typescript
1// Direct value (shorthand for eq)2{ status: "active" }3 4// Explicit operator5{ status: { eq: "active" } }

#Comparison Operators

Operator Description
eq Equal
ne Not equal
gt Greater than
gte Greater than or equal
lt Less than
lte Less than or equal

#Array Operators

Operator Description
inArray Value in list
notInArray Value not in list

#Null Operators

Operator Description
isNull Value is NULL
isNotNull Value is not NULL

#Range Operators

Operator Description
between Value in range (inclusive). Accepts [min, max]
notBetween Value outside range. Accepts [min, max]

#String Operators

Operator Description
like Pattern match (case-sensitive)
notLike Negated pattern match (case-sensitive)
ilike Pattern match (case-insensitive)
notIlike Negated pattern match (case-insensitive)
contains Case-insensitive substring match. Equivalent to ilike: '%value%'
startsWith Case-insensitive prefix match. Equivalent to ilike: 'value%'
endsWith Case-insensitive suffix match. Equivalent to ilike: '%value'

#PostgreSQL Array Operators

Operator Description
arrayContains Column contains all elements of the given array
arrayContained Given array contains all elements of the column
arrayOverlaps Column shares any element with the given array

#Logical Operators

Combine conditions with and, or, or negate with not:

typescript
1{2  and: [3    { status: { eq: "active" } },4    { or: [5      { role: { eq: "admin" } },6      { role: { eq: "moderator" } },7    ]},8  ],9}

You can also pass a raw Drizzle SQLWrapper as the where clause for full SQL control.

#Transactions

Use the transaction method to execute multiple operations atomically:

typescript
1await this.repo.transaction(async (tx) => {2  const user = await this.users.create({ name: "Alice" }, { tx });3  await this.orders.create({ userId: user.id, total: 50 }, { tx });4});

All repository methods accept { tx } in their options parameter to participate in the transaction.

For transactions with built-in retry on version conflicts, use the $transaction primitive:

typescript
 1import { $transaction } from "alepha/orm"; 2  3class OrderService { 4  processOrder = $transaction({ 5    handler: async (tx, orderId: string) => { 6      const order = await this.orders.getById(orderId, { tx }); 7      await this.orders.updateById(orderId, { status: "processed" }, { tx }); 8      return order; 9    },10  });11}

$transaction automatically retries when a DbVersionMismatchError occurs.

#Repository.of

For inline repository creation without a separate entity variable:

typescript
1class App {2  users = $inject(Repository.of(userEntity));3}

This creates a Repository subclass bound to the given entity, suitable for use with $inject.

#Events

Repository operations emit lifecycle events:

Event Payload
repository:create:before { tableName, data }
repository:create:after { tableName, data, entity }
repository:update:before { tableName, where, data }
repository:update:after { tableName, where, data, entities }
repository:delete:before { tableName, where }
repository:delete:after { tableName, where, ids }
repository:read:before { tableName, query }
repository:read:after { tableName, query, entities }

#Error Types

Error Thrown When
DbEntityNotFoundError getOne, getById, updateOne, deleteById find no match
DbVersionMismatchError save detects a version conflict (optimistic locking)
DbConflictError Unique constraint violation
DbForeignKeyError Foreign key constraint violation
DbNotNullError NOT NULL constraint violation
DbDeadlockError Database deadlock detected
DbTableNotFoundError Referenced table does not exist
DbColumnNotFoundError Referenced column does not exist