4.2 KiB
Bank repository
Purpose
BankRepository owns the SurrealDB bank table. It extends
BaseRepository<BankRecord> and exposes the bank-specific
read/write surface that the plugin's service layer and HTTP routes
consume: typed create and update, lookups by name, site, sort
code, status; softDelete (sets status = 1); hasActiveAccounts
(used as the precondition for softDelete); a case-insensitive
searchByName; and the idempotent getOrCreateDefaultBank that the
plugin's onReady hook calls during bootstrap.
Features
- Typed
create(data: CreateBank)/update(id, data: UpdateBank)with site-id normalisation and field-whitelist enforcement. - Sort-code lookup gates on the
XX-XX-XXregex and throwsBankingError(INVALID_SORT_CODE)for malformed input — never silently passes through to the SurQL planner. hasActiveAccountsreads thebank_accounttable via parameterised SurQL so the soft-delete precondition is enforced inside the same request.- OTel
banking.banks_total{op}counter emitted on every successful create/update/soft-delete so observability is on the hot path. deleteByTenantreturns the hard-deleted row count so the plugin'sonTenantDeletehook can log the cleanup volume.
Usage: Basic
import { BankRepository } from "@suaveplan/api-plugin-banking";
const repo = new BankRepository(adapter);
const bank = await repo.create({ name: "Genesis Bank", sort_code: 234567 });
await repo.softDelete(bank.id);
Usage: Advanced
Bootstrap the configured default bank in a lifecycle hook:
import { BankRepository } from "@suaveplan/api-plugin-banking";
async function bootstrap(repo: BankRepository) {
const bank = await repo.getOrCreateDefaultBank("Test Bank", 123456);
return bank;
}
Sort-code-driven lookup:
const found = await repo.getBySortCode("12-34-56");
// throws BankingError(INVALID_SORT_CODE) if format is wrong
Tenant cleanup (used by the plugin's onTenantDelete hook):
const count = await repo.deleteByTenant("site:abcdef");
logger.info({ count }, "banks purged for tenant");
API Reference
class BankRepository extends BaseRepository<BankRecord>
Constructor: new BankRepository(db: DatabaseAdapter).
Inherits everything from BaseRepository and adds:
create(data: CreateBank): Promise<BankRecord>update(id, data: UpdateBank): Promise<BankRecord | null>(typed override)existsByName(name, siteId?): Promise<boolean>getByName(name): Promise<BankRecord | null>getBySiteId(siteId): Promise<BankRecord[]>getBySortCode(sortCode: string): Promise<BankRecord | null>— formattedXX-XX-XXinputgetByStatus(status, limit?, offset?): Promise<BankRecord[]>getActiveBanks(limit?, offset?): Promise<BankRecord[]>getAvailableBanks(limit?, offset?): Promise<BankRecord[]>softDelete(id): Promise<BankRecord | null>hasActiveAccounts(bankId): Promise<boolean>searchByName(searchTerm, limit?): Promise<BankRecord[]>getOrCreateDefaultBank(name, sortCode): Promise<BankRecord>deleteByTenant(tenantId): Promise<number>— count of deleted rows
Static helper: BankRepository.extractBareId(id): string.
Implementation Notes
The sort-code field is stored as int in SurrealDB (matching the
upstream surql/tables/bank-account.surql schema), so the wire-format
XX-XX-XX string is parsed to a number at every lookup site. The
regex guard rejects partial / malformed strings up front; a stray
12-345-6 would otherwise parseInt to 12345 and silently return
the wrong bank.
getOrCreateDefaultBank is idempotent by design — the plugin's
onReady runs it on every host start, and consumers that run multi-host
deployments rely on the "look-up-first, then insert" pattern to avoid
sort-code collisions. This is not a race-free guarantee (two hosts
booting at exactly the same time can both observe missing → both
insert), but the underlying SurQL INSERT will reject on the unique
index in that case, falling back to the lookup-then-fetch path.
hasActiveAccounts queries bank_account directly rather than going
through BankAccountRepository.findBy(...) to avoid the constructor
dependency between repositories — both can be instantiated
independently with only the shared DatabaseAdapter.