Skip to content

DataSources

A DataSource manages database connections and supports schema auto-discovery from repositories.

Connectors

This guide covers the PostgreSQL connector (BasePostgresDataSource, aliased as BaseDataSource for backward compatibility). It's the primary relational engine and the one used by most applications. IGNIS also ships a SQLite connector (BaseSqliteDataSource, see SQLite) and a typesense connector for full-text/vector search (see Search & Typesense). All three implement the same engine-neutral AbstractDataSource contract - see Connectors for the architecture.

Creating a DataSource

typescript
// src/datasources/postgres.datasource.ts
import {
  BasePostgresDataSource,
  datasource,
  ValueOrPromise,
} from '@venizia/ignis';
import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
import { Pool } from 'pg';

interface IDSConfigs {
  host: string;
  port: number;
  database: string;
  user: string;
  password: string;
}

@datasource({ driver: NodePostgresDriver })
export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
  constructor() {
    super({
      name: PostgresDataSource.name,
      config: {
        host: process.env.APP_ENV_POSTGRES_HOST ?? 'localhost',
        port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
        database: process.env.APP_ENV_POSTGRES_DATABASE ?? 'mydb',
        user: process.env.APP_ENV_POSTGRES_USERNAME ?? 'postgres',
        password: process.env.APP_ENV_POSTGRES_PASSWORD ?? '',
      },
      // No schema needed - auto-discovered from @repository bindings!
    });
  }

  override configure(): ValueOrPromise<void> {
    const schema = Object.keys(this.getSchema());
    this.logger.debug('[configure] Auto-discovered schema | Keys: %o', schema);

    // That is all - naming NodePostgresDriver above is what wires the driver and connector.
    this.client = new Pool(this.settings);
  }

  override getConnectionString(): ValueOrPromise<string> {
    const { host, port, user, password, database } = this.settings;
    return `postgresql://${user}:${password}@${host}:${port}/${database}`;
  }
}

Driver seam: the raw client goes on `this.client`

this.client = new Pool(...) is the short path: configure() builds only the client. getConnector()/beginTransaction() lazily instantiate the class named in @datasource({ driver }) over it - NodePostgresDriver here. There is no pool field - the raw-client slot is client, whatever the client happens to be. Naming the driver class (rather than a driver-name string) is what carries pg into the app's bundle. A bundler only packages a real value reference, never text. The alternative is to wire a driver yourself for a custom or third-party driver: configure() calls this.useDriver({ driver, schema? }). That assigns this.driver and builds this.connector in one step (so the half-wired state cannot exist), bypassing @datasource({ driver }) entirely. See Postgres Drivers & Supabase for postgres-js and Supabase.

How auto-discovery works:

  1. @repository decorators register model-datasource bindings in the MetadataRegistry
  2. getSchema() invokes discoverSchema() which calls MetadataRegistry.buildSchema({ dataSource }) to collect all bound models and their relations
  3. The lazily-built Drizzle connector is initialized with the complete schema (tables + Drizzle relations)

You can disable auto-discovery per datasource via @datasource({ driver: NodePostgresDriver, autoDiscovery: false }).

Manual Schema (Optional)

If you need explicit control, you can still provide schema manually:

typescript
@datasource({ driver: NodePostgresDriver })
export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
  constructor() {
    super({
      name: PostgresDataSource.name,
      config: { /* ... */ },
      schema: {
        User: User.schema,
        Configuration: Configuration.schema,
        // Add relations if using Drizzle's relational queries
      },
    });
  }
}

DataSource Hierarchy

AbstractDataSource extends BaseHelper        # engine-neutral, src/base - no pool, no Drizzle
  └── AbstractPostgresDataSource              # connectors/postgres - adds pool, connector
        └── BasePostgresDataSource (alias: BaseDataSource)
              ├── configure()               # Assign this.client (abstract) - base wires driver + connector
              ├── getConnectionString()     # Build connection URL (abstract)
              ├── getSchema()               # Auto-discover from @repository bindings
              ├── discoverSchema()          # Internal: reads MetadataRegistry
              ├── hasDiscoverableModels()   # Check if any repos reference this DS
              ├── getCapabilities()         # Returns { transactions: true }
              ├── beginTransaction(opts?)   # Start transaction with isolation level
              ├── getConnector()            # Get Drizzle connector
              └── getSettings()            # Get connection config

Registering a DataSource

typescript
// src/application.ts
export class Application extends BaseApplication {
  preConfigure(): ValueOrPromise<void> {
    this.dataSource(PostgresDataSource);
  }
}

DataSources are bound as singletons to ensure connection pool sharing across the application.

Supported Engines

EngineDriver/PackageImportStatus
PostgreSQLnode-postgres (pg)@venizia/ignis or @venizia/ignis/postgresSupported, transactions + 3 isolation levels
Typesense (search)typesense (optional peer)@venizia/ignis/typesense (subpath-only)Supported, no transactions/locks
MySQL / SQLite--Not planned; would be a new connector under src/connectors/

DataSource Template

typescript
import { BasePostgresDataSource, datasource, ValueOrPromise } from '@venizia/ignis';
import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
import { Pool } from 'pg';

interface IDSConfigs {
  host: string;
  port: number;
  database: string;
  user: string;
  password: string;
}

@datasource({ driver: NodePostgresDriver })
export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
  constructor() {
    super({
      name: PostgresDataSource.name,
      config: {
        host: process.env.APP_ENV_POSTGRES_HOST ?? 'localhost',
        port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
        database: process.env.APP_ENV_POSTGRES_DATABASE ?? 'mydb',
        user: process.env.APP_ENV_POSTGRES_USERNAME ?? 'postgres',
        password: process.env.APP_ENV_POSTGRES_PASSWORD ?? '',
      },
    });
  }

  override configure(): ValueOrPromise<void> {
    this.client = new Pool(this.settings);
  }

  override getConnectionString(): ValueOrPromise<string> {
    const { host, port, user, password, database } = this.settings;
    return `postgresql://${user}:${password}@${host}:${port}/${database}`;
  }
}

Deep Dive: See BaseDataSource Reference for connection pooling and advanced configuration.

See Also