Initial Commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# 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
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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):
|
||||
|
||||
```ts
|
||||
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`.
|
||||
Reference in New Issue
Block a user