Pular para o conteúdo

Decorators de Coluna

Os decorators de coluna definem o “corpo” da sua entidade: quais propriedades são persistidas, como os dados do banco são convertidos para JavaScript e como o Mirror gerencia campos de auditoria e controle de concorrência.


Mapeia uma propriedade da classe para uma coluna no banco de dados. Aceita três formas:

// 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;
OpçãoTipoPadrãoDescrição
namestringnome da propriedadeNome da coluna no banco de dados
nullablebooleanfalsePermite que a propriedade seja null
typeColumnTypeundefinedConversão de tipo aplicada pelo JIT Hydrator
selectbooleantrueSe false, a coluna é omitida do SELECT por padrão

Por padrão, o banco retorna tudo como string. A opção type instrui o JIT Hydrator a converter o valor durante a hidratação, sem loops de verificação em runtime.

typeRecebido do bancoValor em JS
'number''123'123
'bigint''9999999999999'9999999999999n
'boolean'1 / 0 / 'true'true / false
'datetime'string ISO ou DateDate
'date'Date'YYYY-MM-DD' (string)
'iso'Datestring ISO UTC
'string'qualquerString(value)

Use select: false para colunas que nunca devem aparecer em queries padrão, como hashes de senha:

@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'],
});

Nota: Passar select explicitamente sobrescreve completamente o comportamento de select: false. Liste todas as colunas que deseja, incluindo as normais.

O driver pg converte colunas TIMESTAMP para Date automaticamente, mas descarta os microssegundos. Se você precisa de precisão de µs (ex: logs de auditoria):

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'

Define a chave primária da entidade e sua estratégia de geração.

@PrimaryColumn({ strategy: 'uuid_v7' })
id!: string;
OpçãoTipoDescrição
namestringNome da coluna no banco
strategyGenerationStrategyComo o ID é gerado (ver tabela abaixo)
generate() => string | numberFunção de geração customizada (obrigatório com 'custom')
typeColumnTypeTipo de conversão (útil com 'bigint' para BIGSERIAL)
strategyGerado porQuandoIndicado para
'identity'Banco de dadosNo INSERTAuto-increment simples (SERIAL, AUTO_INCREMENT)
'uuid_v4'MirrorAntes do INSERTIDs aleatórios universais
'uuid_v7'MirrorAntes do INSERTIDs ordenáveis no tempo — recomendado para tabelas grandes
'ulid'MirrorAntes do INSERTIDs compactos, ordenáveis, amigáveis a URL
'cuid2'MirrorAntes do INSERTIDs seguros e resistentes a colisões
'custom'Sua funçãoAntes do INSERTQualquer lógica proprietária

Com 'identity', o Mirror omite a coluna do INSERT e recupera o ID gerado pelo banco. O mecanismo de recuperação varia por dialect:

DialectMecanismo
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;

O Mirror oferece três decorators que gerenciam campos de auditoria automaticamente. Os valores são gerados em JavaScript (não via DEFAULT do banco), garantindo consistência entre dialects e integrando com o dirty checking.

Preenche a propriedade com new Date() uma única vez, no momento do INSERT. Nunca é atualizado.

@CreatedAt()
createdAt!: Date;
// Coluna customizada no banco:
@CreatedAt('created_at_utc')
createdAt!: Date;

Coluna padrão no banco: created_at

Atualiza a propriedade com new Date() em todo UPDATE, inclusive em updates parciais.

@UpdatedAt()
updatedAt!: Date;

Coluna padrão no banco: updated_at

Adicionar @DeletedAt ativa o soft delete na entidade inteira. O Mirror passa a filtrar automaticamente registros com deleted_at IS NOT NULL em todas as 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 });

Coluna padrão no banco: deleted_at


Controla concorrência otimista. O Mirror verifica se a versão no banco ainda é a mesma que está na instância antes de aplicar o UPDATE. Se divergir, lança OptimisticLockError.

import { Entity, PrimaryColumn, Column, VersionColumn } from 'mirror-orm';
@Entity('products')
class Product {
@PrimaryColumn({ strategy: 'identity' })
id!: number;
@Column()
stock!: number;
@VersionColumn()
version!: number;
}

Coluna padrão no banco: version A coluna deve existir no banco como 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

O UPDATE gerado sempre inclui a versão atual como condição do WHERE. Se nenhuma linha for afetada (versão divergiu), o Mirror detecta e lança o erro antes de commitar qualquer mudança.

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 } });
// ...
}
}

Após um save() bem-sucedido, o Mirror incrementa automaticamente o version na instância em memória — você não precisa recarregar o objeto para continuar usando.

Ver também: Para cenários de concorrência mais complexos com locks de banco, consulte Transactions.