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.