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.
@Column
Seção intitulada “@Column”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ção | Tipo | Padrão | Descrição |
|---|---|---|---|
name | string | nome da propriedade | Nome da coluna no banco de dados |
nullable | boolean | false | Permite que a propriedade seja null |
type | ColumnType | undefined | Conversão de tipo aplicada pelo JIT Hydrator |
select | boolean | true | Se false, a coluna é omitida do SELECT por padrão |
Type Casting
Seção intitulada “Type Casting”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.
type | Recebido do banco | Valor em JS |
|---|---|---|
'number' | '123' | 123 |
'bigint' | '9999999999999' | 9999999999999n |
'boolean' | 1 / 0 / 'true' | true / false |
'datetime' | string ISO ou Date | Date |
'date' | Date | 'YYYY-MM-DD' (string) |
'iso' | Date | string ISO UTC |
'string' | qualquer | String(value) |
select: false — Colunas Sensíveis
Seção intitulada “select: false — Colunas Sensíveis”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ã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'],});Nota: Passar
selectexplicitamente sobrescreve completamente o comportamento deselect: false. Liste todas as colunas que deseja, incluindo as normais.
Timestamps de alta precisão com pg
Seção intitulada “Timestamps de alta precisão com pg”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 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
Seção intitulada “@PrimaryColumn”Define a chave primária da entidade e sua estratégia de geração.
@PrimaryColumn({ strategy: 'uuid_v7' })id!: string;| Opção | Tipo | Descrição |
|---|---|---|
name | string | Nome da coluna no banco |
strategy | GenerationStrategy | Como o ID é gerado (ver tabela abaixo) |
generate | () => string | number | Função de geração customizada (obrigatório com 'custom') |
type | ColumnType | Tipo de conversão (útil com 'bigint' para BIGSERIAL) |
Estratégias de Geração
Seção intitulada “Estratégias de Geração”strategy | Gerado por | Quando | Indicado para |
|---|---|---|---|
'identity' | Banco de dados | No INSERT | Auto-increment simples (SERIAL, AUTO_INCREMENT) |
'uuid_v4' | Mirror | Antes do INSERT | IDs aleatórios universais |
'uuid_v7' | Mirror | Antes do INSERT | IDs ordenáveis no tempo — recomendado para tabelas grandes |
'ulid' | Mirror | Antes do INSERT | IDs compactos, ordenáveis, amigáveis a URL |
'cuid2' | Mirror | Antes do INSERT | IDs seguros e resistentes a colisões |
'custom' | Sua função | Antes do INSERT | Qualquer 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:
| Dialect | Mecanismo |
|---|---|
| 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;Decorators de Auditoria
Seção intitulada “Decorators de Auditoria”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.
@CreatedAt
Seção intitulada “@CreatedAt”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
@UpdatedAt
Seção intitulada “@UpdatedAt”Atualiza a propriedade com new Date() em todo UPDATE, inclusive em updates parciais.
@UpdatedAt()updatedAt!: Date;Coluna padrão no banco:
updated_at
@DeletedAt — Soft Delete
Seção intitulada “@DeletedAt — Soft Delete”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 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 });Coluna padrão no banco:
deleted_at
@VersionColumn — Optimistic Locking
Seção intitulada “@VersionColumn — Optimistic Locking”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:
versionA coluna deve existir no banco comoINTEGER NOT NULL DEFAULT 0.
Como funciona
Seção intitulada “Como funciona”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 OptimisticLockErrorO 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.