Files
suaveplan/packages/api/api-plugin-banking/src/repositories/bank-repository.md
2026-05-27 23:29:53 +01:00

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-XX regex and throws BankingError(INVALID_SORT_CODE) for malformed input — never silently passes through to the SurQL planner.
  • hasActiveAccounts reads the bank_account table 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.
  • deleteByTenant returns the hard-deleted row count so the plugin's onTenantDelete hook 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> — formatted XX-XX-XX input
  • getByStatus(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.