Skip to content

Deep Dive: DataSources

Technical reference for DataSource classes - managing database connections in Ignis.

Files: packages/core/src/base/datasources/*.ts

Quick Reference

Class/InterfacePurposeKey Members
IDataSourceContract for all datasourcesname, settings, connector, getSchema(), configure(), beginTransaction()
AbstractDataSourceBase implementation with loggingExtends BaseHelper, declares abstract methods
BaseDataSourceConcrete class to extendAuto-discovery, transaction support, constructor with config
ITransactionTransaction objectconnector, isActive, isolationLevel, commit(), rollback()
IsolationLevelsIsolation level constantsREAD_COMMITTED, REPEATABLE_READ, SERIALIZABLE

IDataSource Interface

Contract for all datasource classes in the framework.

File: packages/core/src/base/datasources/common/types.ts

Type Parameters

typescript
interface IDataSource<
  Settings extends object = {},
  Schema extends TAnyDataSourceSchema = TAnyDataSourceSchema,
  ConfigurableOptions extends object = {},
> extends IConfigurable<ConfigurableOptions>
Type ParameterDefaultDescription
Settings{}Connection configuration type (host, port, etc.)
SchemaTAnyDataSourceSchemaCombined Drizzle schema type (tables + relations)
ConfigurableOptions{}Options passed to configure()

Properties & Methods

MemberTypeDescription
namestringDatasource name
settingsSettingsConfiguration object
connectorTNodePostgresConnector<Schema>Drizzle ORM connector instance
schemaSchemaCombined Drizzle schema (auto-discovered or manual)
getSchema()SchemaReturns combined Drizzle schema
getSettings()SettingsReturns connection settings
getConnector()TNodePostgresConnector<Schema>Returns the Drizzle connector
getConnectionString()ValueOrPromise<string>Returns connection string
configure(opts?)ValueOrPromise<void>Initializes pool and connector
beginTransaction(opts?)Promise<ITransaction<Schema>>Starts a new database transaction

AbstractDataSource & BaseDataSource

File: packages/core/src/base/datasources/base.ts

AbstractDataSource

Top-level abstract class that implements IDataSource. Extends BaseHelper for scoped logging. Declares the core properties and abstract methods.

typescript
abstract class AbstractDataSource<
  Settings extends object = {},
  Schema extends TAnyDataSourceSchema = TAnyDataSourceSchema,
  ConfigurableOptions extends object = {},
> extends BaseHelper implements IDataSource<Settings, Schema, ConfigurableOptions>

Properties:

PropertyTypeVisibilityDescription
namestringpublicDatasource identifier
settingsSettingspublicConnection configuration
connectorTNodePostgresConnector<Schema>publicDrizzle ORM instance
schemaSchemapublicCombined schema (tables + relations)
poolPoolprotectednode-postgres connection pool

Abstract methods (must be implemented by subclasses):

MethodReturn TypeDescription
configure(opts?)ValueOrPromise<void>Initialize pool and Drizzle connector
getConnectionString()ValueOrPromise<string>Return the database connection URL
beginTransaction(opts?)Promise<ITransaction<Schema>>Start a new transaction

Concrete methods:

MethodReturn TypeDescription
getSettings()SettingsReturns this.settings
getConnector()TNodePostgresConnector<Schema>Returns this.connector
getSchema()SchemaReturns this.schema (throws if not initialized)

BaseDataSource

Extends AbstractDataSource with a constructor, schema auto-discovery, and a default beginTransaction() implementation.

typescript
abstract class BaseDataSource<
  Settings extends object = {},
  Schema extends TAnyDataSourceSchema = TAnyDataSourceSchema,
  ConfigurableOptions extends object = {},
> extends AbstractDataSource<Settings, Schema, ConfigurableOptions>

Key Features

FeatureDescription
Schema Auto-DiscoverySchema is automatically built from registered @repository decorators
Manual OverrideYou can manually provide schema in constructor for full control
Built-in Transaction SupportbeginTransaction() implemented using the pool property

TIP

Set autoDiscovery to false in the @datasource decorator to disable automatic schema discovery. This is useful when you want to manually provide the schema.

Constructor Options

typescript
constructor(opts: {
  name: string;           // DataSource name (usually class name)
  config: Settings;       // Database connection settings
  schema?: Schema;        // Optional - auto-discovered if not provided
})

Schema Auto-Discovery

When you use @repository({ model: YourModel, dataSource: YourDataSource }), the framework automatically:

  1. Registers the model-datasource binding in the MetadataRegistry
  2. When getSchema() is called, discovers all models bound to this datasource
  3. Builds the combined schema (tables + relations) automatically

This means you no longer need to manually merge tables and relations in the DataSource constructor!

Configuration Flow

  1. Your DataSource's constructor is called:

    • You call super() with name and config
    • Schema is auto-discovered from @repository bindings (or manually provided via schema)
  2. Application.registerDataSources() is called during startup:

    • The application gets your DataSource instance from the DI container
    • It calls the configure() method on your instance
  3. Your configure() method runs:

    • Call this.getSchema() to get the auto-discovered schema
    • Create a Pool instance and assign it to this.pool (required for transaction support)
    • Create the Drizzle connector with the pool and schema

Example Implementations

Simplest approach - schema is auto-discovered from repositories:

typescript
// src/datasources/postgres.datasource.ts
import { BaseDataSource, datasource, ValueOrPromise } from '@venizia/ignis';
import { applicationEnvironment, int } from '@venizia/ignis-helpers';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

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

/**
 * PostgresDataSource with auto-discovery support.
 *
 * How it works:
 * 1. @repository decorator binds model to datasource
 * 2. When configure() is called, getSchema() auto-discovers all bound models
 * 3. Drizzle is initialized with the auto-discovered schema
 */
@datasource({ driver: 'node-postgres' })
export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
  private readonly protocol = 'postgresql';

  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'),
        ssl: false,
      },
      // NO schema property - auto-discovered from @repository bindings!
    });
  }

  override configure(): ValueOrPromise<void> {
    // getSchema() auto-discovers models from @repository bindings
    const schema = this.getSchema();

    const dsSchema = Object.keys(schema);
    this.logger.debug(
      '[configure] Auto-discovered schema | Schema + Relations (%s): %o',
      dsSchema.length,
      dsSchema,
    );

    // Store pool reference for transaction support
    this.pool = new Pool(this.settings);
    this.connector = drizzle({ client: this.pool, schema });
  }

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

With this pattern, when you define repositories:

typescript
@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> {}

The PostgresDataSource.schema will automatically include User and Configuration tables and their relations.

Pattern 2: Manual Schema (Full Control)

When you need explicit control over schema (e.g., subset of models, custom ordering):

typescript
import {
  User, userTable, userRelations,
  Configuration, configurationTable, configurationRelations,
} from '@/models/entities';

@datasource({ driver: 'node-postgres' })
export class PostgresDataSource extends BaseDataSource<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 ?? '',
      },
      // Manually provide schema using spread syntax
      schema: {
        [User.TABLE_NAME]: userTable,
        [Configuration.TABLE_NAME]: configurationTable,
        ...userRelations.relations,
        ...configurationRelations.relations,
      },
    });
  }

  override configure(): ValueOrPromise<void> {
    // When schema is manually provided, getSchema() returns it directly
    this.pool = new Pool(this.settings);
    this.connector = drizzle({ client: this.pool, schema: this.getSchema() });
  }

  override getConnectionString(): ValueOrPromise<string> {
    // ...
  }
}

IMPORTANT

You must assign this.pool in your configure() method. The built-in beginTransaction() uses this.pool to acquire a PoolClient for transaction isolation. If this.pool is not set, beginTransaction() will throw an error.

@datasource Decorator

The @datasource decorator registers datasource metadata:

typescript
@datasource({
  driver: 'node-postgres',       // Required - database driver
  autoDiscovery?: true           // Optional - defaults to true
})
OptionTypeDefaultDescription
driverTDataSourceDriver-Database driver name (currently only 'node-postgres')
autoDiscoverybooleantrueEnable/disable schema auto-discovery

Abstract Methods

When extending BaseDataSource, these methods must be implemented:

MethodReturn TypeDescription
configure(opts?)ValueOrPromise<void>Initialize pool and Drizzle connector. Must set this.pool and this.connector.
getConnectionString()ValueOrPromise<string>Return the database connection string.

Helper Methods

MethodDescription
getSchema()Returns the schema (auto-discovers via discoverSchema() if not manually provided)
getSettings()Returns connection settings
getConnector()Returns the Drizzle connector
hasDiscoverableModels()Returns true if there are models registered for this datasource via @repository

Protected Methods

MethodDescription
discoverSchema()Queries the MetadataRegistry for all @repository bindings targeting this datasource, then calls registry.buildSchema() to merge tables and relations into a single schema object.

Connector Types

File: packages/core/src/base/datasources/common/types.ts

TypeDescription
TNodePostgresConnector<Schema>Drizzle connector using NodePgClient (Pool or PoolClient)
TNodePostgresTransactionConnector<Schema>Drizzle connector using PoolClient specifically (for transaction isolation)
TAnyConnector<Schema>Union of both connector types
TAnyDataSourceSchemaRecord<string, any> — base type for all schema objects

DataSourceDrivers

Static class for driver validation:

typescript
DataSourceDrivers.NODE_POSTGRES  // 'node-postgres'
DataSourceDrivers.isValid('node-postgres')  // true

Transaction Support

DataSources provide built-in transaction management through the beginTransaction() method. This allows you to perform atomic operations across multiple repositories.

How It Works

BaseDataSource.beginTransaction() does the following:

  1. Acquires a PoolClient from this.pool
  2. Executes BEGIN TRANSACTION ISOLATION LEVEL <level> on the client
  3. Creates a separate Drizzle connector scoped to that client
  4. Returns an ITransaction object with commit(), rollback(), and the scoped connector

When commit() or rollback() is called, the client is released back to the pool.

Transaction Types

TypeDescription
ITransaction<Schema>Transaction object with commit(), rollback(), and connector
ITransactionOptionsOptions for starting a transaction (e.g., isolationLevel)
TIsolationLevelUnion type: 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE'
IsolationLevelsStatic class with isolation level constants and validation

ITransaction Interface

typescript
interface ITransaction<Schema> {
  connector: TNodePostgresTransactionConnector<Schema>;
  isActive: boolean;       // read-only getter, false after commit/rollback
  isolationLevel: TIsolationLevel;

  commit(): Promise<void>;
  rollback(): Promise<void>;
}

Isolation Levels

Use the IsolationLevels static class for type-safe isolation level constants:

typescript
import { IsolationLevels } from '@venizia/ignis';

// Available levels
IsolationLevels.READ_COMMITTED   // Default - prevents dirty reads
IsolationLevels.REPEATABLE_READ  // Consistent reads within transaction
IsolationLevels.SERIALIZABLE     // Strictest isolation

// Validation
IsolationLevels.isValid('READ COMMITTED'); // true
IsolationLevels.isValid('INVALID');        // false

NOTE

The default isolation level is READ COMMITTED when no isolationLevel option is provided.

Usage Example

typescript
// Start transaction from datasource or repository
const tx = await dataSource.beginTransaction({
  isolationLevel: IsolationLevels.SERIALIZABLE
});

try {
  // Use tx.connector for operations
  await tx.connector.insert(userTable).values({ name: 'Alice' });
  await tx.connector.insert(profileTable).values({ userId: '...', bio: 'Hello' });

  await tx.commit();
} catch (error) {
  await tx.rollback();
  throw error;
}

Note: For most use cases, prefer using repository.beginTransaction() which provides a higher-level API. See Repositories Reference for details.

This architecture ensures that datasources are configured consistently and that the fully-initialized Drizzle connector, aware of all schemas and relations, is available to repositories for querying.

See Also