Overview
@sqb/connect ships a decorator-based ORM, layered directly on top of its own connection/pooling
classes and @sqb/builder's query builder. It gives you typed entity classes, a CRUD
Repository generated from them, relationship loading, and a handful of lifecycle hooks — without
giving up the query builder underneath, or introducing a schema/config file separate from your
code.
Why an ORM on top of the query builder
The query builder and connection layer already let you run any query against any of SQB's
supported databases — but using them directly for everyday CRUD means writing the same shapes of
code, by hand, for every table: a Select/Insert/Update/Delete per operation, a manual
mapping from raw rows to your own types, a hand-written "insert then re-fetch by key" for getting
the created row back, and hand-written joins for anything you want to load alongside it.
SQB's ORM removes that repetition. You describe a table once, as a class with decorators:
import { BaseEntity, Column, Entity, PrimaryKey, Link } from '@sqb/connect';
import type { Country } from './country.entity.js';
@Entity('customers')
export class Customer extends BaseEntity {
@PrimaryKey()
@Column()
declare id: number;
@Column()
declare givenName: string;
@Column()
declare familyName: string;
@(Link().toOne(() => Country, { sourceKey: 'countryCode', targetKey: 'code' }))
declare readonly country?: Country;
}
...and get a Repository<Customer> that knows how to build and run every CRUD statement for it,
and how to convert rows back into Customer instances:
const repo = client.getRepository(Customer);
const customer = await repo.create({ givenName: 'Jane', familyName: 'Doe', countryCode: 'US' });
const found = await repo.findById(customer.id, { projection: ['givenName', 'country'] });
await repo.update(customer.id, { familyName: 'Smith' });
await repo.delete(customer.id);
Nothing about the query builder goes away: a Repository builds ordinary @sqb/builder queries
under the hood using the entity's metadata, and you can drop down to
Select/Insert/Update/Delete directly at any time — from
inside entity code or alongside it — and both layers interoperate freely.
Defining a model: entities
A model — SQB calls it an entity — is a plain TypeScript class whose properties are annotated with decorators that describe how the class maps to a database table. No schema file, no separate mapping config: the class itself, plus its decorators, is the mapping. Every property on an entity is one of three kinds, plus a handful of entity-level concerns layered on top:
| Concept | Decorator(s) | Covers |
|---|---|---|
| Entities | @Entity | Registers a class as an entity, sets its table name/schema/comment. |
| Data Columns | @Column | Maps a property to a plain column — data type, nullability, default value, insert/update participation. |
| Embedded Objects | @Embedded | Groups a set of columns on the same table under a nested object property, without a join. |
| Associations | @Link | Relates an entity to rows in another table — to-one, to-many, or a multi-hop chain — resolved lazily, only when a query's projection asks for it. |
| Primary Keys | @PrimaryKey | Marks one or more columns as the entity's primary index, used to resolve a row by key. |
| Indexes and Foreign Keys | @Index, @ForeignKey | Records index and foreign-key metadata — informational, and available to tooling built on top of entities. |
| Lifecycle Hooks | @BeforeInsert, @AfterInsert, @BeforeUpdate, @AfterUpdate, @BeforeDestroy, @AfterDestroy | Methods invoked around a Repository's create/update/delete operations. |
| Entity Composition | Entity.mixin, Entity.Pick, Entity.Omit, Entity.Union | Builds new entity classes out of existing ones, carrying over both the class members and the entity metadata. |
Decorators only ever describe metadata — applying @Entity/@Column/@Link to a class doesn't
touch a database, open a connection, or run any SQL by itself. BaseEntity (extended by
Customer above) is an optional base class that adds a couple of convenience instance methods
(destroy(), exists()); see Defining Models for the full model.
Reading and writing rows: Repository
Metadata becomes useful once you ask a client or connection for a
Repository for the class — that's the object that actually reads the
metadata to build queries, execute them, and map rows back onto instances of your entity:
const repo = client.getRepository(Customer);
// or, inside a transaction/acquired connection:
const repo = connection.getRepository(Customer);
Repository<T> covers the full CRUD surface — create()/createOnly(), findById()/
findOne()/findMany(), update()/updateOnly()/updateMany(), delete()/deleteMany(),
plus count() and exists()/existsOne(). Every read/write method takes a filter (a plain
object, an @sqb/builder operator, or a dotted path reaching into an embedded object or an
association) and, for reads, a projection that both narrows the columns fetched and decides
which associations get resolved for that call. See Repositories for the full
method-by-method reference, filters, and projection.
How it fits with the rest of SQB
The ORM is part of @sqb/connect — the same package as the connection pool, transactions, and
cursors/streaming — so a Repository uses whichever client/connection you already have. See
Connecting & Pooling for how to construct a
SqbClient, pool connections, and run transactions; entities and repositories work the same way
inside a transaction as they do on a plain client.
Where to go next
- Defining Models — entities, data columns, embedded objects, associations, primary keys, indexes/foreign keys, lifecycle hooks, and entity composition.
- Repositories — reading and writing entity rows.