docs(db): owned-lookup call-site convention; refresh side effect; e2e ORM module spread notes

This commit is contained in:
jubnl
2026-09-22 12:53:47 +02:00
parent 4209343328
commit b0905f9867
5 changed files with 25 additions and 3 deletions
@@ -20,6 +20,9 @@ export class AppSettingsRepository extends EntityRepository<AppSettings> {
* the same key inside the same request, which is exactly the coexistence
* case this plan is built on (raw SQL and the ORM sharing one connection
* while callers convert one at a time). `refresh` keeps this a single
* Side effect of `refresh`: an UNFLUSHED in-memory change to the selected
* field on that entity is discarded (the entity reverts to the row). No
* caller mutates these entities before reading, by design (D4: rows out).
* query either way; only a stale identity-map hit is avoided.
*/
async getValue(key: string): Promise<string | null> {
@@ -22,6 +22,9 @@ export class UsersRepository extends EntityRepository<Users> {
* from the identity map on a repeat call, which would hide a raw/native
* `UPDATE users SET email = ...` on the same id inside the same request.
* `refresh` keeps this a single query and always sees the current row.
* Side effect of `refresh`: an UNFLUSHED in-memory change to the selected
* field on that entity is discarded (the entity reverts to the row). No
* caller mutates these entities before reading, by design (D4: rows out).
*/
async getEmail(userId: number): Promise<string | null> {
const row = await this.findOne({ id: userId }, { fields: ['email'], refresh: true });
@@ -21,6 +21,18 @@ import type { EntityRepository, FilterQuery, FilterValue, OrderDefinition } from
* /`TagsRepository`, which stay the actual public API and must run the result
* through `toRow` before it leaves the repository.
*
* Call-site convention (Task 0 re-review): type arguments are ALWAYS explicit —
* inference from the repository argument alone resolves `T` to the
* `{ id: unknown }` constraint and fails to compile (TS2345) — and the owner
* field is the RELATION property (`'user'`), never the `persist(false)` twin
* (`'user_id'`): both compile and emit the same SQL, but only the relation
* name survives a column rename and reads as the entity's API. `ownerId` is the
* raw FK number. Canonical calls:
*
* listForOwner<Tags, 'user', 'name'>(tags, 'user', userId, 'name');
* findOwnedByUser<Tags, 'user'>(tags, id, 'user', userId);
* findOwnedOrGlobal<Categories, 'user'>(categories, id, 'user', userId);
*
* Typing (Task 0 review, I2): `T extends { id: unknown }` and `K extends
* keyof T` tie `ownerField` to a real property of `T` and `ownerId` to
* `FilterValue<T[K]>` — the same value shape MikroORM's own `FilterObject`
+5 -2
View File
@@ -101,10 +101,13 @@ export async function createTestOrm(
* middleware, since D6 needs the per-request EM fork this default provides)
* changes what `buildApp()` registers. Building this helper from the same
* `mikroOrmConfig` object production uses means that fix reaches both at
* once; if it is ever done as a hand-edit instead, this helper's 15 e2e
* harnesses would silently stop mirroring production and need the same
* once; if it is ever done as a hand-edit instead, the e2e harnesses built
* on this helper would silently stop mirroring production and need the same
* `allowGlobalContext`-style re-justification `createTestOrm` above got.
*/
// The spread also carries production's `extensions` (Migrator, SeedManager),
// `migrations` and `seeder` settings into the harness; only the driver, the
// database name, the entity list and discovery noise are overridden.
export function createTestMikroOrmModule(db: Database.Database): DynamicModule | Promise<DynamicModule> {
return MikroOrmModule.forRoot({
...mikroOrmConfig,
@@ -30,7 +30,8 @@ describe('listForOwner', () => {
createTag(testDb, owner.id, { name: 'Apple' });
createTag(testDb, other.id, { name: 'Middle' });
// T/K/OF all inferred: T from `tags`, K from the 'user' literal, OF from 'name'.
// Type arguments are always explicit: inference from the `tags` argument alone
// resolves T to the `{ id: unknown }` constraint (TS2345), never to the entity.
const rows = await listForOwner<Tags, 'user', 'name'>(tags, 'user', owner.id, 'name');
expect(rows.map((r) => r.name)).toEqual(['Apple', 'Zebra']);
expect(rows.every((r) => r.user_id === owner.id)).toBe(true);