mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
7ca7dfd555
Brings in the Tours addon and the MikroORM follow-ups. Conflicts were resolved to dev's content, then every file dev brought in was run through prettier and eslint --fix so the branch stays uniformly formatted.
199 lines
8.6 KiB
TypeScript
199 lines
8.6 KiB
TypeScript
/**
|
|
* `Migration20200101042000_tours`: the Tours tables on a fresh install, and on
|
|
* a test instance that already carries them from the pre-ORM Tours branch.
|
|
*
|
|
* Migrate to the step immediately before it, optionally lay down the
|
|
* branch's own tables with rows in them, apply just this migration, assert.
|
|
*/
|
|
import { createMigrationOrm, migrateTo, pendingNames, rawExec, rawQuery } from '../../helpers/migration-step';
|
|
import type { MikroORM } from '@mikro-orm/sqlite';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const TARGET = 'Migration20200101042000_tours';
|
|
|
|
async function ormBeforeTarget(): Promise<MikroORM> {
|
|
const orm = await createMigrationOrm();
|
|
const names = await pendingNames(orm);
|
|
const idx = names.indexOf(TARGET);
|
|
expect(idx).toBeGreaterThan(0);
|
|
await migrateTo(orm, names[idx - 1]);
|
|
return orm;
|
|
}
|
|
|
|
async function indexNames(orm: MikroORM, table: string): Promise<string[]> {
|
|
const rows = await rawQuery<{ name: string }>(orm, `SELECT name FROM pragma_index_list('${table}') ORDER BY name`);
|
|
return rows.map((r) => r.name);
|
|
}
|
|
|
|
async function seedTripWithPlace(orm: MikroORM): Promise<void> {
|
|
await rawExec(orm, "INSERT INTO users (id, username, email, password_hash) VALUES (1, 'owner', 'owner@test', 'x')");
|
|
await rawExec(orm, "INSERT INTO trips (id, user_id, title) VALUES (10, 1, 'Alps')");
|
|
await rawExec(orm, "INSERT INTO places (id, trip_id, name) VALUES (100, 10, 'Ridge walk')");
|
|
}
|
|
|
|
describe('Tours migration', () => {
|
|
it('TOURMIG-001: a fresh install gets the three tables, the hike type and no redundant waypoint index', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
await migrateTo(orm, TARGET);
|
|
|
|
expect(
|
|
await rawQuery(
|
|
orm,
|
|
'SELECT key, label_key, icon, color, routing_profile, is_sport, enabled, sort_order FROM tour_types',
|
|
),
|
|
).toEqual([
|
|
{
|
|
key: 'hike',
|
|
label_key: 'tourTypes.hike',
|
|
icon: 'Mountain',
|
|
color: '#16a34a',
|
|
routing_profile: 'pedestrian',
|
|
is_sport: 1,
|
|
enabled: 1,
|
|
sort_order: 0,
|
|
},
|
|
]);
|
|
|
|
const tourColumns = await rawQuery<{ name: string; notnull: number; dflt_value: string | null; pk: number }>(
|
|
orm,
|
|
'SELECT name, "notnull", dflt_value, pk FROM pragma_table_info(\'tours\') ORDER BY cid',
|
|
);
|
|
expect(tourColumns.map((c) => c.name)).toEqual([
|
|
'place_id',
|
|
'tour_type',
|
|
'distance',
|
|
'elevation_gain',
|
|
'elevation_loss',
|
|
'duration',
|
|
'difficulty',
|
|
'wanderer_ref',
|
|
'match_confidence',
|
|
'tour_group_id',
|
|
'created_at',
|
|
'max_hiking_difficulty',
|
|
]);
|
|
expect(tourColumns.find((c) => c.name === 'max_hiking_difficulty')).toMatchObject({
|
|
notnull: 1,
|
|
dflt_value: '2',
|
|
});
|
|
expect(tourColumns.find((c) => c.name === 'place_id')).toMatchObject({ pk: 1 });
|
|
|
|
expect(await indexNames(orm, 'tours')).toContain('idx_tours_tour_type');
|
|
expect(await indexNames(orm, 'tour_waypoints')).not.toContain('idx_tour_waypoints_place');
|
|
|
|
const fks = await rawQuery<{ table: string; from: string; to: string; on_delete: string }>(
|
|
orm,
|
|
'SELECT "table", "from", "to", on_delete FROM pragma_foreign_key_list(\'tours\') UNION ALL SELECT "table", "from", "to", on_delete FROM pragma_foreign_key_list(\'tour_waypoints\') ORDER BY 1, 2',
|
|
);
|
|
expect(fks).toEqual([
|
|
{ table: 'places', from: 'place_id', to: 'id', on_delete: 'CASCADE' },
|
|
{ table: 'tour_types', from: 'tour_type', to: 'key', on_delete: 'NO ACTION' },
|
|
{ table: 'tours', from: 'place_id', to: 'place_id', on_delete: 'CASCADE' },
|
|
]);
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
|
|
it('TOURMIG-002: the CHECKs refuse out-of-range values and a place delete cascades through tours to its waypoints', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
await migrateTo(orm, TARGET);
|
|
await rawExec(orm, 'PRAGMA foreign_keys = ON');
|
|
await seedTripWithPlace(orm);
|
|
|
|
await expect(
|
|
rawExec(orm, "INSERT INTO tours (place_id, tour_type, max_hiking_difficulty) VALUES (100, 'hike', 7)"),
|
|
).rejects.toThrow(/CHECK/);
|
|
await expect(rawExec(orm, "INSERT INTO tours (place_id, tour_type) VALUES (100, 'kayak')")).rejects.toThrow(
|
|
/FOREIGN KEY/,
|
|
);
|
|
await rawExec(orm, "INSERT INTO tours (place_id, tour_type) VALUES (100, 'hike')");
|
|
expect(await rawQuery(orm, 'SELECT max_hiking_difficulty FROM tours')).toEqual([{ max_hiking_difficulty: 2 }]);
|
|
|
|
await expect(
|
|
rawExec(orm, "INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (100, 91, 0, 'start', 0)"),
|
|
).rejects.toThrow(/CHECK/);
|
|
await expect(
|
|
rawExec(orm, "INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (100, 0, 0, 'detour', 0)"),
|
|
).rejects.toThrow(/CHECK/);
|
|
await rawExec(
|
|
orm,
|
|
"INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (100, 47, 11, 'start', 0), (100, 47.1, 11.1, 'end', 1)",
|
|
);
|
|
await expect(
|
|
rawExec(orm, "INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (100, 47, 11, 'via', 1)"),
|
|
).rejects.toThrow(/UNIQUE/);
|
|
|
|
await rawExec(orm, 'DELETE FROM places WHERE id = 100');
|
|
expect(await rawQuery(orm, 'SELECT COUNT(*) AS n FROM tours')).toEqual([{ n: 0 }]);
|
|
expect(await rawQuery(orm, 'SELECT COUNT(*) AS n FROM tour_waypoints')).toEqual([{ n: 0 }]);
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
|
|
it('TOURMIG-003: on an instance that already has the pre-ORM Tours tables it keeps every row and drops only the redundant index', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
// The pre-ORM branch's final shape, rows and all, with its custom hike row.
|
|
await rawExec(
|
|
orm,
|
|
`CREATE TABLE tour_types (
|
|
key TEXT PRIMARY KEY, label_key TEXT NOT NULL, icon TEXT NOT NULL, color TEXT NOT NULL, routing_profile TEXT,
|
|
is_sport INTEGER NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0)`,
|
|
);
|
|
await rawExec(
|
|
orm,
|
|
"INSERT INTO tour_types (key, label_key, icon, color, routing_profile) VALUES ('hike', 'tourTypes.hike', 'Mountain', '#000000', 'pedestrian')",
|
|
);
|
|
await rawExec(
|
|
orm,
|
|
`CREATE TABLE tours (
|
|
place_id INTEGER PRIMARY KEY REFERENCES places(id) ON DELETE CASCADE,
|
|
tour_type TEXT NOT NULL REFERENCES tour_types(key), distance REAL, elevation_gain REAL, elevation_loss REAL,
|
|
duration REAL, difficulty TEXT, wanderer_ref TEXT, match_confidence REAL, tour_group_id INTEGER,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
max_hiking_difficulty INTEGER NOT NULL DEFAULT 2 CHECK(max_hiking_difficulty BETWEEN 1 AND 6))`,
|
|
);
|
|
await rawExec(orm, 'CREATE INDEX idx_tours_tour_type ON tours(tour_type)');
|
|
await rawExec(
|
|
orm,
|
|
`CREATE TABLE tour_waypoints (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
place_id INTEGER NOT NULL REFERENCES tours(place_id) ON DELETE CASCADE,
|
|
lat REAL NOT NULL CHECK(lat >= -90 AND lat <= 90), lng REAL NOT NULL CHECK(lng >= -180 AND lng <= 180),
|
|
role TEXT NOT NULL CHECK(role IN ('start', 'via', 'end')), sequence INTEGER NOT NULL CHECK(sequence >= 0),
|
|
UNIQUE(place_id, sequence))`,
|
|
);
|
|
await rawExec(orm, 'CREATE INDEX idx_tour_waypoints_place ON tour_waypoints(place_id, sequence)');
|
|
await seedTripWithPlace(orm);
|
|
await rawExec(
|
|
orm,
|
|
"INSERT INTO tours (place_id, tour_type, distance, max_hiking_difficulty) VALUES (100, 'hike', 12.5, 4)",
|
|
);
|
|
await rawExec(
|
|
orm,
|
|
"INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence) VALUES (100, 47, 11, 'start', 0), (100, 47.1, 11.1, 'end', 1)",
|
|
);
|
|
|
|
await migrateTo(orm, TARGET);
|
|
|
|
expect(await rawQuery(orm, 'SELECT key, color FROM tour_types')).toEqual([{ key: 'hike', color: '#000000' }]);
|
|
expect(await rawQuery(orm, 'SELECT place_id, tour_type, distance, max_hiking_difficulty FROM tours')).toEqual([
|
|
{ place_id: 100, tour_type: 'hike', distance: 12.5, max_hiking_difficulty: 4 },
|
|
]);
|
|
expect(await rawQuery(orm, 'SELECT role, sequence FROM tour_waypoints ORDER BY sequence')).toEqual([
|
|
{ role: 'start', sequence: 0 },
|
|
{ role: 'end', sequence: 1 },
|
|
]);
|
|
expect(await indexNames(orm, 'tours')).toContain('idx_tours_tour_type');
|
|
expect(await indexNames(orm, 'tour_waypoints')).not.toContain('idx_tour_waypoints_place');
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
});
|