DataSources
A datasource owns the connection to a database (or search engine).
In one example
The smallest real datasource: a pg.Pool wrapped in a class, wired to node-postgres via @datasource.
import { Pool } from 'pg';
import { datasource } from '@venizia/ignis';
import { BasePostgresDataSource } from '@venizia/ignis/postgres';
import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
interface IDataSourceConfigs {
host: string;
port: number;
database: string;
user: string;
password: string;
}
@datasource({ driver: NodePostgresDriver })
export class PostgresDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
override configure(): void {
this.client = new Pool(this.settings);
}
override getConnectionString(): string {
const { host, port, user, password, database } = this.settings;
return `postgresql://${user}:${password}@${host}:${port}/${database}`;
}
}A @repository binds a model to PostgresDataSource, and the schema is auto-discovered from that binding - no manual schema wiring.
How it works
- Driver is a class, not a string.
@datasource({ driver })names a driver class -NodePostgresDriver,PostgresJsDriver,PGliteDriver,LibSqlDriver- never a driver-name string. Only a class reference carriespg/postgres/@electric-sql/pglite/@libsql/clientinto the bundle, keeping all four packages optional. configure()has exactly one job. Build the raw client and assign it tothis.client(apg.Poolfor node-postgres, or a postgres-jsSql). It never touchesthis.connectordirectly.- The driver wires lazily. The first time
getConnector()orbeginTransaction()is called, the base class reads the class named in@datasource({ driver }). It instantiates that class overthis.clientand buildsthis.connectorfrom it. stop()closes it.application.stop()callsclose()on every datasource the boot configured, which ends the client through the driver. You write no shutdown code. A datasource you configure by hand inpostConfigure()is yours to close.- Three layers, not two. IGNIS splits datasources into an engine-neutral root (
AbstractDataSource- no SQL, no Drizzle, no pool), an engine-neutral relational tier (AbstractRelationalDataSource->BaseRelationalDataSource, which owns the driver seam, schema auto-discovery and transactions), and one branch per engine. See Connectors for the full architecture. - Naming.
BasePostgresDataSourceis a subclass of the neutralBaseRelationalDataSource, not an alias for it. It adds the Postgres dialect, executor andBEGIN TRANSACTION ISOLATION LEVELstatement.BaseDataSourceis the one true alias here - the same class under its historical name. The neutralBaseRelationalDataSourcelives at@venizia/ignis/relationaland is not re-exported from@venizia/ignis/postgres.
Common tasks
Configure connection settings
Pass connection settings through the constructor's config option. configure() reads them back off this.settings.
import { Pool } from 'pg';
import { applicationEnvironment, int } from '@venizia/ignis-helpers';
export class PostgresDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
constructor() {
super({
name: PostgresDataSource.name,
config: {
host: applicationEnvironment.get<string>('APP_ENV_POSTGRES_HOST'),
port: int(applicationEnvironment.get<string>('APP_ENV_POSTGRES_PORT')),
database: applicationEnvironment.get<string>('APP_ENV_POSTGRES_DATABASE'),
user: applicationEnvironment.get<string>('APP_ENV_POSTGRES_USERNAME'),
password: applicationEnvironment.get<string>('APP_ENV_POSTGRES_PASSWORD'),
},
});
}
override configure(): void {
this.client = new Pool(this.settings);
}
}Choose a driver
| Driver | Import from | Package | When to use |
|---|---|---|---|
NodePostgresDriver | @venizia/ignis/postgres/node-postgres | pg | Long-standing default |
PostgresJsDriver | @venizia/ignis/postgres/postgres-js | postgres | Required for Supabase's transaction pooler; faster elsewhere |
PGliteDriver | @venizia/ignis/postgres/pglite | @electric-sql/pglite | Postgres compiled to WASM, in-process. Tests and local runs with no server |
LibSqlDriver | @venizia/ignis/sqlite/libsql | @libsql/client | SQLite: :memory:, a local file, remote Turso, or an embedded replica |
PGliteDriver and LibSqlDriver each hold one session, so acquire() hands out a one-slot pool. Write through acquire() when a transaction may be open.
Swapping drivers only changes which class @datasource names and how configure() builds the client:
import { PostgresJsDriver } from '@venizia/ignis/postgres/postgres-js';
import postgres from 'postgres';
import type { Sql } from 'postgres';
import * as schema from '@/schemas';
@datasource({ driver: PostgresJsDriver })
export class PostgresDataSource extends BasePostgresDataSource<
IDataSourceConfigs,
typeof schema,
{},
Sql
> {
override configure(): void {
this.client = postgres(this.getConnectionString());
}
}See Postgres Drivers & Supabase for the full comparison, client-validation behavior, and Supabase presets.
Run a transaction
beginTransaction() acquires a connection, issues BEGIN, and returns a handle with a scoped connector, commit(), and rollback().
import { IsolationLevels } from '@venizia/ignis/postgres';
import { userTable } from '@/schemas';
const transaction = await postgresDataSource.beginTransaction({
isolationLevel: IsolationLevels.SERIALIZABLE,
});
try {
await transaction.connector.insert(userTable).values({ name: 'Alice' });
await transaction.commit();
} catch (error) {
try {
await transaction.rollback();
} catch (rollbackError) {
console.error('Rollback failed | %s', rollbackError);
}
throw error;
}rollback()throws on failure. Nest it in its owntry...catchso a rollback failure never replaces the original error.- Prefer the repository API. Most repository code should call
repository.beginTransaction()instead of going through the datasource directly. See Transactions.
Share one datasource across repositories
One PostgresDataSource instance is shared by every repository bound to it. @repository auto-injects the datasource, and getSchema() merges the tables and relations of every model bound to it.
@repository({ model: User, dataSource: PostgresDataSource })
export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
@repository({ model: Configuration, dataSource: PostgresDataSource })
export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {}PostgresDataSource.schema automatically includes both User and Configuration, plus their relations - one pooled connection, no per-repository connection setup.
See also
- Full reference - every
IDataSourcemember,BasePostgresDataSourceinternals, and transaction edge cases - Tutorial - creating datasources step by step
- Connectors - the base-vs-connector architecture
- Repositories - the layer that queries through a datasource
- Models - the schema a datasource discovers from
@repositorybindings
Files:
packages/kernel/src/base/datasources/abstract.ts- neutralAbstractDataSourcepackages/connectors/src/relational/core/datasources/base.ts- neutralBaseRelationalDataSource- schema auto-discovery, transactions,getCapabilities()packages/connectors/src/relational/postgres/datasources/base.ts- PostgreSQLBasePostgresDataSourcepackages/connectors/src/relational/sqlite/datasources/base.ts- SQLiteBaseSqliteDataSource