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 })namesNodePostgresDriverorPostgresJsDriveras a class reference - never a driver-name string. Only a real class reference carriespg/postgresinto the application's bundle, so both stay genuinely optional peer dependencies. 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 }), instantiates it overthis.client, and buildsthis.connectorfrom it. - Base vs. connector split. IGNIS splits datasources into an engine-neutral root (
AbstractDataSource- no SQL, no Drizzle, no pool) and per-engine connectors (BasePostgresDataSourcefor PostgreSQL, a parallel class for typesense). See Connectors for the full architecture. - Naming. The PostgreSQL connector's canonical class is
BaseRelationalDataSource;BasePostgresDataSourceandBaseDataSourceare compatibility aliases re-exporting the same class.
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 | Package | When to use |
|---|---|---|
NodePostgresDriver | pg | Long-standing default |
PostgresJsDriver | postgres | Required for Supabase's transaction pooler; faster elsewhere |
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/core/src/base/datasources/abstract.ts- neutralAbstractDataSourcepackages/core/src/connectors/postgres/datasources/base.ts- PostgreSQLBasePostgresDataSource