Column Decorators
Column decorators define the “body” of your entity: which properties are persisted, how database data is converted to JavaScript, and how Mirror manages auditing and concurrency control fields.
@Column
Section titled “@Column”Maps a class property to a column in the database. Accepts three forms:
// Forma 1: sem argumentos — usa o nome da propriedade como nome da coluna@Column()name!: string;
// Forma 2: nome da coluna no banco como string@Column('display_name')name!: string;
// Forma 3: objeto de opções completo@Column({ name: 'display_name', type: 'number', nullable: true, select: false })price!: number | null;Options
Section titled “Options”| Option | Type | Standard | Description |
|---|---|---|---|
name | string | property name | Column name in database |
nullable | boolean | false | Allows property to be null |
type | ColumnType | undefined | Type conversion applied by JIT Hydrator |
select | boolean | true | If false, the column is omitted from the SELECT by default |
Type Casting
Section titled “Type Casting”By default, the database returns everything as a string. The type option instructs JIT Hydrator to convert the value during hydration, without check loops at runtime.
type | Received from the bank | Value in JS |
|---|---|---|
'number' | '123' | 123 |
'bigint' | '9999999999999' | 9999999999999n |
'boolean' | 1 / 0 / 'true' | true / false |
'datetime' | string ISO or Date | Date |
'date' | Date | 'YYYY-MM-DD' (string) |
'iso' | Date | string ISO UTC |
'string' | any | String(value) |
select: false — Sensitive Columns
Section titled “select: false — Sensitive Columns”Use select: false for columns that should never appear in standard queries, such as password hashes:
@Entity('users')class User { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() email!: string;
@Column({ select: false }) passwordHash!: string;}
// passwordHash nunca vem no SELECT padrãoconst user = await repo.findOne({ where: { id: 1 } });// user.passwordHash === undefined
// Para buscá-la explicitamente:const user = await repo.findOne({ where: { id: 1 }, select: ['id', 'email', 'passwordHash'],});Note: Passing
selectexplicitly completely overrides the behavior ofselect: false. List all the columns you want, including normal ones.
High precision timestamps with pg
Section titled “High precision timestamps with pg”The pg driver converts TIMESTAMP columns to Date automatically, but discards the microseconds. If you need µs precision (ex: audit logs):
import pg from 'pg';
// No bootstrap da aplicação, antes da conexão:pg.types.setTypeParser(1114, (val) => val); // TIMESTAMP sem timezonepg.types.setTypeParser(1184, (val) => val); // TIMESTAMPTZ
// Na entidade, usar type: 'string' para receber o valor bruto@Column({ type: 'string' })createdAt!: string; // '2024-01-15 10:30:00.123456'@PrimaryColumn
Section titled “@PrimaryColumn”Defines the entity’s primary key and its generation strategy.
@PrimaryColumn({ strategy: 'uuid_v7' })id!: string;Options
Section titled “Options”| Option | Type | Description |
|---|---|---|
name | string | Column name in database |
strategy | GenerationStrategy | How the ID is generated (see table below) |
generate | () => string | number | Custom generation function (required with 'custom') |
type | ColumnType | Type conversion (useful with 'bigint' for BIGSERIAL) |
Generation Strategies
Section titled “Generation Strategies”strategy | Generated by | When | Suitable for |
|---|---|---|---|
'identity' | Database | In INSERT | Simple auto-increment (SERIAL, AUTO_INCREMENT) |
'uuid_v4' | Mirror | Before INSERT | Universal Random IDs |
'uuid_v7' | Mirror | Before INSERT | Time-sortable IDs — recommended for large tables |
'ulid' | Mirror | Before INSERT | Compact, Sortable, URL-Friendly IDs |
'cuid2' | Mirror | Before INSERT | Secure, collision-resistant IDs |
'custom' | Your role | Before INSERT | Any proprietary logic |
With 'identity', Mirror omits the INSERT column and retrieves the ID generated by the bank. The recovery mechanism varies by dialect:
| Dialect | Mechanism |
|---|---|
| PostgreSQL | RETURNING id |
| MySQL / MariaDB | LAST_INSERT_ID() |
| SQLite | last_insert_rowid() |
| SQL Server | OUTPUT INSERTED.id |
// Estratégia custom@PrimaryColumn({ strategy: 'custom', generate: () => myIdGenerator() })id!: string;
// BIGSERIAL no PostgreSQL — usar type: 'bigint' para receber o valor correto@PrimaryColumn({ strategy: 'identity', type: 'bigint' })id!: bigint;Audit Decorators
Section titled “Audit Decorators”Mirror offers three decorators that automatically manage audit fields. The values are generated in JavaScript (not via the bank’s DEFAULT), ensuring consistency between dialects and integrating with dirty checking.
@CreatedAt
Section titled “@CreatedAt”Fills the property with new Date() only once, at the time of INSERT. It is never updated.
@CreatedAt()createdAt!: Date;
// Coluna customizada no banco:@CreatedAt('created_at_utc')createdAt!: Date;Default column in the database:
created_at
@UpdatedAtUpdates the property with new Date() in all UPDATE, including partial updates.
Section titled “@UpdatedAtUpdates the property with new Date() in all UPDATE, including partial updates.”@UpdatedAt()updatedAt!: Date;Default column in the database:
updated_at
@DeletedAt — Soft Delete
Section titled “@DeletedAt — Soft Delete”Adding @DeletedAt enables soft delete on the entire entity. Mirror now automatically filters records with deleted_at IS NOT NULL in all queries.
@Entity('posts')class Post { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() title!: string;
@DeletedAt() deletedAt!: Date | null;}
// "Deletar" registra o timestamp, não remove a linhaawait repo.softDelete({ id: 1 });
// Queries normais ignoram registros deletados automaticamenteconst posts = await repo.find(); // só retorna deletedAt = null
// Para incluir deletados:const all = await repo.find({ withDeleted: true });
// Para restaurar:await repo.softRestore({ id: 1 });Default column in the database:
deleted_at
@VersionColumn — Optimistic Locking
Section titled “@VersionColumn — Optimistic Locking”Controls optimistic competition. Mirror checks whether the version in the database is still the same as the one in the instance before applying the UPDATE. If it diverges, throws OptimisticLockError.
import { Entity, PrimaryColumn, Column, VersionColumn } from 'mirror-orm';
@Entity('products')class Product { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() stock!: number;
@VersionColumn() version!: number;}Default column in the database:
versionThe column must exist in the database asINTEGER NOT NULL DEFAULT 0.
How it works
Section titled “How it works”const repo = conn.getRepository(Product);
const product = await repo.findOne({ where: { id: 1 } });// product.version === 3
// Outra instância atualiza o mesmo registro enquanto isso...
product.stock -= 10;
// Mirror gera: UPDATE products SET stock = 90, version = 4// WHERE id = 1 AND version = 3await repo.save(product);// Se o version no banco não for mais 3 → lança OptimisticLockErrorThe generated UPDATE always includes the current version as a condition of WHERE. If no lines are affected (version diverged), Mirror detects and throws the error before committing any changes.
import { OptimisticLockError } from 'mirror-orm';
try { await repo.save(product);} catch (err) { if (err instanceof OptimisticLockError) { // Re-ler o estado atual e tentar novamente const fresh = await repo.findOne({ where: { id: product.id } }); // ... }}After a successful save(), Mirror automatically increments the version in the in-memory instance — you don’t need to reload the object to continue using it.
See also: For more complex concurrency scenarios with bank locks, see Transactions.