Database Wiring
Recipes are working wiring examples with the sharp edges annotated. This page and the
database-wiringrecipe 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. OnlymigrationsRuntoday (auto-run pending migrations on boot, single-node only).DatabaseTypeormModule.forRoot({...})— driver config:type,host/database,synchronize,logging,migrationsDir. PuttingtypeonDatabaseModule(ormigrationsRunon 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.
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>.