4.8 KiB
Database
Negareh API uses PostgreSQL with MikroORM 6. Schema changes are managed through migrations — never rely on synchronize in production.
Connection
Configuration lives in src/config/mikro-orm.config.ts and reads from environment variables:
| Variable | Description |
|---|---|
DB_HOST |
PostgreSQL host |
DB_PORT |
PostgreSQL port |
DB_USER |
Database user |
DB_PASS |
Database password |
DB_NAME |
Database name |
NODE_ENV |
production disables auto schema ensure |
In non-production, MikroORM can auto-create/update the database schema on startup (ensureDatabase). In production, only migrations should change the schema.
Entity conventions
All persistent models extend BaseEntity:
// src/common/entities/base.entity.ts
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
export abstract class BaseEntity {
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
id: string = ulid();
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
createdAt: Date = new Date();
@Property({ nullable: true, columnType: 'timestamptz' })
deletedAt?: Date;
}
Rules
- Primary keys — ULID strings (
char(26)), not auto-increment integers. - Timestamps —
timestamptzwith UTC (forceUtcTimezone: truein config). - Soft deletes — Set
deletedAtinstead of hard-deleting rows. ThenotDeletedfilter excludes soft-deleted records by default. - Indexes — Add
@Indexon foreign keys and columns used inWHERE/ORDER BYclauses. - Table names — Explicit
tableNamein@Entity({ tableName: 'users' })when the name differs from the class.
Relations
- Use MikroORM decorators:
@ManyToOne,@OneToMany,@ManyToMany. - Initialize
OneToManywithnew Collection<T>(this). - Use
populatein queries to load relations and avoid N+1.
Updates
wrap(entity).assign(partialDto);
await em.flush();
Repositories
Custom data access extends EntityRepository<Entity>:
@Injectable()
export class UserRepository extends EntityRepository<User> {
constructor(readonly em: EntityManager) {
super(em, User);
}
async findAllPaginated(dto: FindUsersDto): Promise<PaginatedResult<User>> {
// build FilterQuery, findAndCount with limit/offset
}
}
Register repositories as providers in the feature module alongside MikroOrmModule.forFeature([Entity]).
Migrations
Migrations live in database/migrations/ and are emitted as TypeScript.
Commands
| Script | Action |
|---|---|
npm run migration:create |
Generate migration from entity diff |
npm run migration:blank |
Create empty migration file |
npm run migration:up |
Apply pending migrations |
npm run migration:down |
Revert last migration |
npm run migration:list |
List all migrations |
npm run migration:pending |
Show unapplied migrations |
npm run migration:fresh |
Drop and re-run all migrations |
Guidelines
- Always use migrations for schema changes in shared environments.
- Migrations are transactional (
allOrNothing: true). safe: trueprevents destructive drops in generated migrations — review each file before applying.- Complex logic (triggers, functions) belongs in migrations — see
Migration20260626140000for invoice total recalculation triggers. - Never remove data in migrations unless explicitly requested.
Workflow
- Modify entity files.
- Run
npm run migration:create. - Review the generated SQL in
database/migrations/. - Run
npm run migration:up. - Commit the migration file with the entity changes.
Seeders
Seeders populate development/staging data. Entry point: src/seeders/DatabaseSeeder.ts.
npm run db:seed # Run all seeders
npm run db:reset # Drop + create + migrate + seed (destructive)
Seeder structure:
src/seeders/
├── DatabaseSeeder.ts # Orchestrates seed order
├── <entity>.seeder.ts # Per-entity seed logic
└── data/ # Static seed data arrays
Seed order matters — permissions and roles must exist before admins; categories before products.
Query guidelines
- Avoid
SELECT *in raw queries; MikroORM entity loads are fine but usefieldsoption when you only need a subset. - Design filters to hit indexes (
$ilikeon indexed columns sparingly; prefer exact match on indexed fields). - Use
findAndCountfor paginated lists. - Wrap multi-step writes in
em.transactional(async (em) => { ... }).
Connection pool
Pool settings are tuned per environment in mikro-orm.config.ts:
- Production: min 5, max 20 connections
- Development: min 2, max 10 connections
Statement and idle timeouts are set to 60 seconds to prevent hung transactions.