Skip to main content

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:

ConceptDecorator(s)Covers
Entities@EntityRegisters a class as an entity, sets its table name/schema/comment.
Data Columns@ColumnMaps a property to a plain column — data type, nullability, default value, insert/update participation.
Embedded Objects@EmbeddedGroups a set of columns on the same table under a nested object property, without a join.
Associations@LinkRelates 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@PrimaryKeyMarks one or more columns as the entity's primary index, used to resolve a row by key.
Indexes and Foreign Keys@Index, @ForeignKeyRecords index and foreign-key metadata — informational, and available to tooling built on top of entities.
Lifecycle Hooks@BeforeInsert, @AfterInsert, @BeforeUpdate, @AfterUpdate, @BeforeDestroy, @AfterDestroyMethods invoked around a Repository's create/update/delete operations.
Entity CompositionEntity.mixin, Entity.Pick, Entity.Omit, Entity.UnionBuilds 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.