@Column('numeric', {
name: 'balance',
precision: 20,
scale: 6,
default: () => "'0'", // ← wrap in () => to emit as SQL literal
transformer: { … },
})
public balance!: Decimal; @OneToOne(() => AccountEntity, (acct) => acct.balance, {
onDelete: 'CASCADE',
eager: true, // load balance automatically with account
nullable: false,
})SELECT … FOR UPDATE:async adjustBalance(
accountId: string,
delta: Decimal
): Promise<Decimal> {
return await this.manager.transaction(async (mgr) => {
const bal = await mgr.getRepository(AccountBalanceEntity)
.createQueryBuilder('b')
.setLock('pessimistic_write')
.where('b.accountId = :id', { id: accountId })
.getOneOrFail();
bal.balance = bal.balance.plus(delta);
await mgr.save(bal);
return bal.balance;
});
}ledger_entries (our next table) will atomically update account_balances:account_balance_history table:@Entity('account_balance_history')
export class AccountBalanceHistoryEntity extends BaseModel {
@Column('uuid') accountId!: string;
@Column('numeric', { precision: 20, scale: 6 }) oldBalance!: Decimal;
@Column('numeric', { precision: 20, scale: 6 }) newBalance!: Decimal;
@Column('text') reason!: string; // e.g. 'posted ledger_entry: …'
}AccountBalanceEntity locked down, the natural “Phase 1” step is to implement the SQL trigger above plus the pessimistic-lock helper in your repository. Once that’s in place, balances will stay accurate in real time. After that, we can move on to wiring up your AccountEntity (the one-to-one link) and then build out TransactionTypeEntity and the rest of the stack.