Files
negareh-api/docs/database.md
T
2026-07-02 15:59:46 +03:30

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.
  • Timestampstimestamptz with UTC (forceUtcTimezone: true in config).
  • Soft deletes — Set deletedAt instead of hard-deleting rows. The notDeleted filter excludes soft-deleted records by default.
  • Indexes — Add @Index on foreign keys and columns used in WHERE / ORDER BY clauses.
  • Table names — Explicit tableName in @Entity({ tableName: 'users' }) when the name differs from the class.

Relations

  • Use MikroORM decorators: @ManyToOne, @OneToMany, @ManyToMany.
  • Initialize OneToMany with new Collection<T>(this).
  • Use populate in 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: true prevents destructive drops in generated migrations — review each file before applying.
  • Complex logic (triggers, functions) belongs in migrations — see Migration20260626140000 for invoice total recalculation triggers.
  • Never remove data in migrations unless explicitly requested.

Workflow

  1. Modify entity files.
  2. Run npm run migration:create.
  3. Review the generated SQL in database/migrations/.
  4. Run npm run migration:up.
  5. 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 use fields option when you only need a subset.
  • Design filters to hit indexes ($ilike on indexed columns sparingly; prefer exact match on indexed fields).
  • Use findAndCount for 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.