# Base repository ## Purpose This module owns the shared repository scaffold that `BankRepository` and `BankAccountRepository` both extend. It holds the `DatabaseAdapter` reference, the table name, the SQL-injection-proof field-whitelist guard, generic record-id normalisation helpers, and the common CRUD primitives (`getById`, `getAll`, `count`, `exists`, `update`, `delete`, `findBy`, `findOneBy`). ## Features - Field whitelist enforced on every generated `UPDATE` statement — unknown field names throw `BankingError(INVALID_FIELD_NAME)` rather than reaching the SurQL planner. - Parameterised values via the genesis adapter's positional `$p0, $p1, ...` convention; SQL strings never carry user-supplied data inline. - `flattenRecordIds` collapses RecordId-shaped values (`{ tb, id }`, driver-specific RecordId instances, plain strings) to the canonical `table:id` string at the public boundary. - Errors uniformly wrapped: every thrown SurrealDB error is converted to `BankingError(DATABASE_ERROR)` with `cause` and structured `context` preserved. ## Usage: Basic ```ts import { BaseRepository } from "@suaveplan/api-plugin-banking"; class WidgetRepository extends BaseRepository<{ id: string; name: string }> { protected getAllowedFields(): readonly string[] { return ["name", "updated_at"]; } } const repo = new WidgetRepository(adapter, "widget"); const w = await repo.getById("widget:abc"); await repo.update("widget:abc", { name: "renamed" }); await repo.delete("widget:abc"); ``` ## Usage: Advanced The repository class is generic in the record type. Use the type parameter to enforce the row shape across the public API: ```ts import { BaseRepository, type BankRecord } from "@suaveplan/api-plugin-banking"; class BankRepo extends BaseRepository { protected getAllowedFields(): readonly string[] { return ["name", "sort_code", "site_id", "status", "updated_at"]; } async getActive(): Promise { return this.findBy("status", 4); } } ``` For ad-hoc queries that the base scaffold does not cover, child classes call the protected `runQuery(sql, params)` helper directly: ```ts const rows = await this.runQuery<{ count: number }>( `SELECT count() AS count FROM bank WHERE status = $p0 GROUP ALL`, [4] ); ``` ## API Reference ### `class BaseRepository` `constructor(db: DatabaseAdapter, tableName: string)` Protected `runQuery(sql, params?): Promise` — every child SQL call routes through this so the `DATABASE_ERROR` wrapper is uniform. Protected `validateFieldName(field: string): void` — throws `BankingError(INVALID_FIELD_NAME)` when the name is outside the child's whitelist. Public methods (all `async`, all return flattened records or `null`): - `getById(id)`, `getAll(limit?, offset?)`, `count()`, `exists(id)`, - `update(id, data)`, `delete(id)`, - `findBy(field, value)`, `findOneBy(field, value)`. Abstract `getAllowedFields(): readonly string[]` — child classes return the writable-field whitelist (the union of every column the public surface ever updates plus `updated_at` for the auto-touched timestamp). ### Helpers - `extractId(id: string): string` — strips a `table:` prefix. - `normaliseId(id: string, tableName: string): string` — ensures the prefix is present. - `flattenRecordIds(record): T` — flattens nested RecordId-shaped values to `table:id` strings. ## Implementation Notes The class is `abstract` rather than instantiable because the `getAllowedFields()` contract is load-bearing: any concrete repository that forgets to override it cannot pass field-name validation and so the SQL-injection guard fails closed. This is enforced at the type level (`abstract` method) rather than at runtime. `update()` returns the current record when called with an empty `data` object (after filtering `undefined` and `updated_at`). This is the SQL-side equivalent of a no-op and matches the upstream package's behaviour — call sites that pass `data: {}` get the fetched record back, not `null`. `getAll()` short-circuits to `db.selectAll(table)` when no pagination is requested (the typed adapter path), and only falls back to a raw SQL pagination query when `limit` or `offset` is non-default. This preserves the SurrealDB type narrowing for the common case.