mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
e3982a8e2a
Converts the raw prepare() fixtures and reads in the next set of specs to the ORM factories and readers: the password reset, webauthn, dawarich, budget, to-do and vacay repository specs, the journey, settings, addons and share MCP specs, the plugin POI and operator egress specs, the feeds, storage stats and query helper services, and the packing, collections, day reorder, leg mode, upload context and trip access integration suites. The notifications and google quota e2e suites now seed their users through makeUser with pinned ids, and the notifications suite moves from hand-rolled tables to the migrated snapshot. Legacy statements that serve as a parity oracle and the raw rows the primary-relation spike holds toObject() against stay raw, each with an allow marker naming why.
178 lines
7.2 KiB
TypeScript
178 lines
7.2 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
import { defineEntity, p, wrap, type Ref } from '@mikro-orm/core';
|
|
import { MikroORM } from '@mikro-orm/sqlite';
|
|
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
|
import { resetTestDb } from '../../../helpers/test-db';
|
|
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
|
import { createTrip, createUser } from '../../../helpers/factories';
|
|
import { AppSettings } from '../../../../src/db/entities/AppSettings.entity';
|
|
import { BudgetCategoryOrder } from '../../../../src/db/entities/BudgetCategoryOrder.entity';
|
|
import { VacayUserSettings } from '../../../../src/db/entities/VacayUserSettings.entity';
|
|
|
|
/**
|
|
* Plan 2 (ORM entity rewrite) Task 1 — the primary-relation spike.
|
|
*
|
|
* `BudgetCategoryOrder`/`VacayUserSettings` are the first entities to carry
|
|
* the Phase 0 shape (hidden relation + `persist(false)` scalar twin,
|
|
* snake_case scalars) on a relation that is (part of) the PRIMARY KEY. Before
|
|
* Task 3's bulk rewrite relies on that shape for the ~15 other
|
|
* primary-relation entities, this file pins that it actually works: a hidden
|
|
* PRIMARY relation still hydrates its twin, `findOne`/`find` accept the
|
|
* composite/one-to-one PK condition, and `toObject()` keeps emitting the twin
|
|
* column and omitting the relation — exactly as it does for a non-PRIMARY
|
|
* hidden relation (`DayNotes.day`/`DayNotes.trip`).
|
|
*
|
|
* Also pins the `.name()` vs `.joinColumn()` semantics Task 0 found on
|
|
* `SchoolHolidayRegions.country` (target PK is not `id`): `.name()` is an
|
|
* alias for `.fieldName()`, which a *relation* ignores when computing its FK
|
|
* column (the naming strategy still derives it from the property name + the
|
|
* target's PK) — only `.joinColumn()` overrides the column MikroORM emits.
|
|
*/
|
|
|
|
const testDb = createSnapshotTestDb();
|
|
let t: TestOrm;
|
|
|
|
beforeAll(async () => {
|
|
t = await createTestOrm(testDb);
|
|
});
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
t.clear();
|
|
});
|
|
afterAll(async () => {
|
|
await t.close();
|
|
testDb.close();
|
|
});
|
|
|
|
describe('primary-relation entities (Task 1 spike)', () => {
|
|
it('SPIKE-001: BudgetCategoryOrder — composite PK (trip relation + category text) round-trips create, findOne, find and toObject', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const created = t.em.create(BudgetCategoryOrder, { trip: trip.id, category: 'Food', sort_order: 2 });
|
|
await t.em.persist(created).flush();
|
|
t.em.clear();
|
|
|
|
const rawInsert = testDb
|
|
// test-sql-allow: the raw row is the oracle the ORM's toObject() is held against.
|
|
.prepare('SELECT * FROM budget_category_order WHERE trip_id = ? AND category = ?')
|
|
.get(trip.id, 'Food');
|
|
expect(rawInsert).toEqual({ trip_id: trip.id, category: 'Food', sort_order: 2 });
|
|
|
|
const found = await t.repo(BudgetCategoryOrder).findOne({ trip: trip.id, category: 'Food' });
|
|
expect(found).not.toBeNull();
|
|
|
|
t.em.clear();
|
|
const [hydrated] = await t.repo(BudgetCategoryOrder).find({ trip: trip.id });
|
|
expect(hydrated.trip_id).toBe(trip.id);
|
|
expect(hydrated.trip.isInitialized()).toBe(false);
|
|
expect(wrap(hydrated).toObject()).toStrictEqual(rawInsert);
|
|
});
|
|
|
|
it('SPIKE-002: VacayUserSettings — one-to-one PRIMARY relation (user) round-trips the same four facts', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
const created = t.em.create(VacayUserSettings, {
|
|
user: user.id,
|
|
year_type: 'fiscal',
|
|
year_start_month: 4,
|
|
year_start_day: 1,
|
|
hire_date: '2020-01-01',
|
|
});
|
|
await t.em.persist(created).flush();
|
|
t.em.clear();
|
|
|
|
// test-sql-allow: the raw row is the oracle the ORM's toObject() is held against.
|
|
const rawInsert = testDb.prepare('SELECT * FROM vacay_user_settings WHERE user_id = ?').get(user.id);
|
|
expect(rawInsert).toEqual({
|
|
user_id: user.id,
|
|
year_type: 'fiscal',
|
|
year_start_month: 4,
|
|
year_start_day: 1,
|
|
hire_date: '2020-01-01',
|
|
});
|
|
|
|
const found = await t.repo(VacayUserSettings).findOne({ user: user.id });
|
|
expect(found).not.toBeNull();
|
|
|
|
t.em.clear();
|
|
const [hydrated] = await t.repo(VacayUserSettings).find({ user: user.id });
|
|
expect(hydrated.user_id).toBe(user.id);
|
|
expect(hydrated.user?.isInitialized()).toBe(false);
|
|
expect(wrap(hydrated).toObject()).toStrictEqual(rawInsert);
|
|
});
|
|
|
|
it("SPIKE-003: .name() sets the property's fieldName, but a relation's FK column is only overridden by .joinColumn()", async () => {
|
|
// A throwaway target whose PK is NOT `id` — the exact shape that made
|
|
// SchoolHolidayRegions.country_code (Task 0) a real bug: the naming
|
|
// strategy composes the FK column from the property name plus the
|
|
// target's PK name (`<prop>_<targetPk>`), independent of `.name()`.
|
|
class SpikeTarget {
|
|
code!: string;
|
|
}
|
|
const SpikeTargetSchema = defineEntity({
|
|
class: SpikeTarget,
|
|
properties: {
|
|
code: p.text().primary(),
|
|
},
|
|
});
|
|
|
|
class SpikeSourceName {
|
|
id?: number;
|
|
country!: Ref<SpikeTarget>;
|
|
}
|
|
const SpikeSourceNameSchema = defineEntity({
|
|
class: SpikeSourceName,
|
|
properties: {
|
|
id: p.integer().primary().autoincrement(),
|
|
country: () => p.manyToOne(SpikeTarget).ref().name('country'),
|
|
},
|
|
});
|
|
|
|
class SpikeSourceJoinColumn {
|
|
id?: number;
|
|
country!: Ref<SpikeTarget>;
|
|
}
|
|
const SpikeSourceJoinColumnSchema = defineEntity({
|
|
class: SpikeSourceJoinColumn,
|
|
properties: {
|
|
id: p.integer().primary().autoincrement(),
|
|
country: () => p.manyToOne(SpikeTarget).ref().joinColumn('country'),
|
|
},
|
|
});
|
|
|
|
const spikeOrm = await MikroORM.init({
|
|
entities: [SpikeTargetSchema, SpikeSourceNameSchema, SpikeSourceJoinColumnSchema],
|
|
dbName: ':memory:',
|
|
discovery: { warnWhenNoEntities: false },
|
|
});
|
|
try {
|
|
const namedMeta = spikeOrm.getMetadata().get(SpikeSourceName);
|
|
const joinColumnMeta = spikeOrm.getMetadata().get(SpikeSourceJoinColumn);
|
|
// `.name('country')` is an alias for `.fieldName('country')` (per
|
|
// node_modules/@mikro-orm/core/entity/defineEntity.d.ts) — it does not
|
|
// reach the FK-column computation for a relation, so the naming
|
|
// strategy still derives `<prop>_<targetPk>`.
|
|
expect(namedMeta.properties.country.fieldNames).toEqual(['country_code']);
|
|
// `.joinColumn('country')` overrides the computed FK column directly.
|
|
expect(joinColumnMeta.properties.country.fieldNames).toEqual(['country']);
|
|
} finally {
|
|
await spikeOrm.close(true);
|
|
}
|
|
});
|
|
|
|
it('SPIKE-004: a nullable text PRIMARY KEY (AppSettings.key) round-trips create → findOne unchanged, and toObject keeps the key', async () => {
|
|
const created = t.em.create(AppSettings, { key: 'spike_setting', value: 'on' });
|
|
await t.em.persist(created).flush();
|
|
t.em.clear();
|
|
|
|
const found = await t.repo(AppSettings).findOne({ key: 'spike_setting' });
|
|
expect(found?.key).toBe('spike_setting');
|
|
expect(found?.value).toBe('on');
|
|
|
|
// test-sql-allow: the raw row is the oracle the ORM's toObject() is held against.
|
|
const raw = testDb.prepare('SELECT * FROM app_settings WHERE key = ?').get('spike_setting');
|
|
expect(wrap(found).toObject()).toStrictEqual(raw);
|
|
});
|
|
});
|