Skip to content

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.


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;
OptionTypeStandardDescription
namestringproperty nameColumn name in database
nullablebooleanfalseAllows property to be null
typeColumnTypeundefinedType conversion applied by JIT Hydrator
selectbooleantrueIf false, the column is omitted from the SELECT by default

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.

typeReceived from the bankValue in JS
'number''123'123
'bigint''9999999999999'9999999999999n
'boolean'1 / 0 / 'true'true / false
'datetime'string ISO or DateDate
'date'Date'YYYY-MM-DD' (string)
'iso'Datestring ISO UTC
'string'anyString(value)

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ão
const 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 select explicitly completely overrides the behavior of select: false. List all the columns you want, including normal ones.

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 timezone
pg.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'

Defines the entity’s primary key and its generation strategy.

@PrimaryColumn({ strategy: 'uuid_v7' })
id!: string;
OptionTypeDescription
namestringColumn name in database
strategyGenerationStrategyHow the ID is generated (see table below)
generate() => string | numberCustom generation function (required with 'custom')
typeColumnTypeType conversion (useful with 'bigint' for BIGSERIAL)
strategyGenerated byWhenSuitable for
'identity'DatabaseIn INSERTSimple auto-increment (SERIAL, AUTO_INCREMENT)
'uuid_v4'MirrorBefore INSERTUniversal Random IDs
'uuid_v7'MirrorBefore INSERTTime-sortable IDs — recommended for large tables
'ulid'MirrorBefore INSERTCompact, Sortable, URL-Friendly IDs
'cuid2'MirrorBefore INSERTSecure, collision-resistant IDs
'custom'Your roleBefore INSERTAny proprietary logic

With 'identity', Mirror omits the INSERT column and retrieves the ID generated by the bank. The recovery mechanism varies by dialect:

DialectMechanism
PostgreSQLRETURNING id
MySQL / MariaDBLAST_INSERT_ID()
SQLitelast_insert_rowid()
SQL ServerOUTPUT 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;

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.

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

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 linha
await repo.softDelete({ id: 1 });
// Queries normais ignoram registros deletados automaticamente
const 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


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: version The column must exist in the database as INTEGER NOT NULL DEFAULT 0.

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 = 3
await repo.save(product);
// Se o version no banco não for mais 3 → lança OptimisticLockError

The 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.