Skip to content

Database Wiring

Recipes are working wiring examples with the sharp edges annotated. This page and the database-wiring recipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.

Config splits across two modules:

  • DatabaseModule.forRoot({...}) — universal contract config. Only migrationsRun today (auto-run pending migrations on boot, single-node only).
  • DatabaseTypeormModule.forRoot({...}) — driver config: type, host/database, synchronize, logging, migrationsDir. Putting type on DatabaseModule (or migrationsRun on the driver) silently does nothing.

Entities register through DatabaseEntitiesPool — a value pool (useValue: EntityClass). Using useClass here treats the entity as a service and TypeORM won't see it. Repositories come from the injected DataSource (@Inject(DataSource) then dataSource.getRepository(Entity)); scopedRepository(repo, key, getValue) wraps one with an implicit row filter.

typescript
import { DatabaseModule } from '@modularityjs/database';
import {
  DatabaseEntitiesPool,
  DatabaseTypeormModule,
} from '@modularityjs/database-typeorm';
import { Inject, Injectable } from '@modularityjs/di';
import { Module } from '@modularityjs/modularity';
import {
  Column,
  DataSource,
  Entity,
  PrimaryGeneratedColumn,
  type Repository,
} from 'typeorm';

@Entity()
class Todo {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column('text') // explicit type — decorators can evaluate before reflect-metadata picks up `string`
  title!: string;

  @Column('boolean', { default: false })
  completed!: boolean;
}

@Injectable()
class TodoService {
  private readonly repo: Repository<Todo>;

  constructor(@Inject(DataSource) dataSource: DataSource) {
    this.repo = dataSource.getRepository(Todo);
  }
  list() {
    return this.repo.find();
  }
}

@Module({
  name: 'todos',
  imports: [DatabaseTypeormModule], // pool owner — NOT DatabaseModule
  providers: [TodoService],
  pools: [{ pool: DatabaseEntitiesPool, key: 'todo', useValue: Todo }], // value pool
})
class TodosModule {}

// modules: [
//   ...
//   DatabaseModule.forRoot({ migrationsRun: false }),                    // universal
//   DatabaseTypeormModule.forRoot({ type: 'sqljs', synchronize: true }), // driver-specific (dev/test only)
//   TodosModule,
// ]

synchronize: true is dev-only; production uses migrations registered via migrationPool(migrations) in a module's pools. @Column() without an explicit SQL type ('text', 'boolean', 'int', etc.) only works when TypeScript emits design-type metadata before the entity class evaluates — passing the type defensively avoids the load-order trap.

Seeding follows the same pool pattern — contribute Seeder subclasses to DatabaseSeedersPool (owned by DatabaseModule, not the driver) and run them with pnpm modularity cli database:seed. Seeders constructor-inject their own persistence handles and must be idempotent (upsert, not blind-insert); DatabaseConfig.seedingEnabled guards production (--force bypasses). Scaffold one with pnpm modularity generate seeder <name>.