Web: Suche per Schaltflaeche, Relevanz, alternative Titel, Verlage, Filter, Nachladen, leere Zustaende, URL/Reload/Zurueck, Offline. API: RPC als anon ueber Kong inkl. Fehlerfaellen. Eigene Datensets je Spec-Datei. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
178 lines
7.1 KiB
TypeScript
178 lines
7.1 KiB
TypeScript
/**
|
|
* Testdaten fuer die Katalogsuche (KAT-01, Issue #3, PR 3c). NUR lokal/CI (`withDb`, als `postgres`).
|
|
*
|
|
* Jede Spec-Datei bekommt ein EIGENES Datenset (eigener UUID-Praefix, eigenes Kunstwort), weil die
|
|
* Dateien unter `fullyParallel` gleichzeitig laufen: Raeumt eine Datei auf, darf sie keine Zeilen der
|
|
* anderen loeschen. Das Kunstwort steht in allen Titeln, Verlagen und Begriffen; Suchen mit dem
|
|
* Kunstwort treffen deshalb nie Startbestandsspiele, nie die Fixtures von pgTAP (`qxz`) und nie das
|
|
* Datenset der anderen Datei. Ohne Suchbegriff wird nur ueber die eigenen Genres/Mechaniken gefiltert.
|
|
*
|
|
* Aufraeumen ueber den UUID-Praefix (Spiele, Verlage, Genres, Mechaniken); Zuordnungen und
|
|
* alternative Titel haengen per `on delete cascade` an den Spielen. `seed` loescht zuerst, ist also
|
|
* nach einem abgebrochenen Lauf idempotent.
|
|
*/
|
|
import type { Client } from 'pg';
|
|
import { withDb } from './db';
|
|
|
|
export interface CatalogDatasetOptions {
|
|
/** 8 Hex-Zeichen, UUID-Praefix aller Zeilen des Sets. */
|
|
prefix: string;
|
|
/** Kunstwort (nur Kleinbuchstaben a-z), steht in jedem Titel. */
|
|
word: string;
|
|
}
|
|
|
|
const uuid = (prefix: string, n: number) => `${prefix}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`;
|
|
const cap = (w: string) => w.charAt(0).toUpperCase() + w.slice(1);
|
|
|
|
/** Anzahl der Massenspiele (Seitengroesse 30 + 6, KAT-01 Kriterium 5). */
|
|
export const MASS_COUNT = 36;
|
|
|
|
export function catalogDataset({ prefix, word }: CatalogDatasetOptions) {
|
|
const W = cap(word);
|
|
const id = (n: number) => uuid(prefix, n);
|
|
const mass = Array.from({ length: MASS_COUNT }, (_, i) => {
|
|
const nn = String(i + 1).padStart(2, '0');
|
|
return { id: id(1000 + i + 1), title: `${W} Massenspiel ${nn}` };
|
|
});
|
|
const ids = {
|
|
cafe: id(1),
|
|
wald: id(2),
|
|
rot: id(3),
|
|
blau: id(4),
|
|
familie: id(5),
|
|
experte: id(6),
|
|
duo: id(7),
|
|
relExact: id(10),
|
|
relBegins: id(11),
|
|
relWord: id(12),
|
|
relContains: id(13),
|
|
relPublisher: id(14),
|
|
hidden: id(20),
|
|
merged: id(21),
|
|
genreA: id(101),
|
|
genreB: id(102),
|
|
mechanicM1: id(111),
|
|
mechanicM2: id(112),
|
|
pubQuarz: id(201),
|
|
pubNebel: id(202),
|
|
pubRelevanz: id(203),
|
|
};
|
|
const t = {
|
|
cafe: `${W} Café Größe: Duel®`,
|
|
wald: `${W} Schattenwald`,
|
|
waldAltDe: `${W} Dunkelforst`,
|
|
waldAltEn: `${W} Shadowwood`,
|
|
rot: `${W} Rotkehlchen`,
|
|
blau: `${W} Blaumeise`,
|
|
familie: `${W} Familienfest`,
|
|
experte: `${W} Expertenrunde`,
|
|
duo: `${W} Duo`,
|
|
relExact: `${W} Relevanz`,
|
|
relBegins: `${W} Relevanz Deluxe`,
|
|
relWord: `Die ${W} Relevanz`,
|
|
relContains: `X${W} Relevanzen`,
|
|
relPublisher: `${W} Fremdtitel`,
|
|
hidden: `${W} Verborgen`,
|
|
merged: `${W} Zusammengefuehrt`,
|
|
genreA: `${W} Genre A`,
|
|
genreB: `${W} Genre B`,
|
|
mechanicM1: `${W} Mechanik 1`,
|
|
mechanicM2: `${W} Mechanik 2`,
|
|
pubQuarz: `Quarzfeld ${W} Presse`,
|
|
pubNebel: `Nebelkamm ${W} Verlag`,
|
|
pubRelevanz: `Relevanz Verlag ${W}`,
|
|
};
|
|
return { word, W, prefix, ids, titles: t, mass };
|
|
}
|
|
|
|
export type CatalogDataset = ReturnType<typeof catalogDataset>;
|
|
|
|
/** Loescht alle Zeilen des Sets (anhand des UUID-Praefixes). */
|
|
export async function cleanupCatalog(c: Client, ds: Pick<CatalogDataset, 'prefix'>): Promise<void> {
|
|
const like = `${ds.prefix}-%`;
|
|
await c.query(`delete from public.games where id::text like $1`, [like]);
|
|
await c.query(`delete from public.publishers where id::text like $1`, [like]);
|
|
await c.query(`delete from public.genres where id::text like $1`, [like]);
|
|
await c.query(`delete from public.mechanics where id::text like $1`, [like]);
|
|
}
|
|
|
|
/** Legt das Datenset an (nach Aufraeumen). */
|
|
export async function seedCatalog(ds: CatalogDataset): Promise<void> {
|
|
const { ids: i, titles: t } = ds;
|
|
await withDb(async (c) => {
|
|
await cleanupCatalog(c, ds);
|
|
await c.query(
|
|
`insert into public.genres (id, name_de, name_en) values ($1, $2, $2), ($3, $4, $4)`,
|
|
[i.genreA, t.genreA, i.genreB, t.genreB],
|
|
);
|
|
await c.query(
|
|
`insert into public.mechanics (id, name_de, name_en) values ($1, $2, $2), ($3, $4, $4)`,
|
|
[i.mechanicM1, t.mechanicM1, i.mechanicM2, t.mechanicM2],
|
|
);
|
|
await c.query(
|
|
`insert into public.publishers (id, name) values ($1, $2), ($3, $4), ($5, $6)`,
|
|
[i.pubQuarz, t.pubQuarz, i.pubNebel, t.pubNebel, i.pubRelevanz, t.pubRelevanz],
|
|
);
|
|
|
|
// (id, title, year, min_p, max_p, min_min, max_min, min_age)
|
|
const games: [string, string, number | null, number | null, number | null, number | null, number | null, number | null][] = [
|
|
[i.cafe, t.cafe, 2021, 2, 2, 20, 30, 12],
|
|
[i.wald, t.wald, 2019, 2, 4, 45, 60, 10],
|
|
[i.rot, t.rot, 2018, 2, 4, 30, 45, 8],
|
|
[i.blau, t.blau, 2022, 2, 5, 30, 45, 8],
|
|
[i.familie, t.familie, 2017, 3, 8, 20, 45, 6],
|
|
[i.experte, t.experte, 2016, 1, 2, 120, 180, 14],
|
|
[i.duo, t.duo, 2015, 2, 2, 30, 30, 8],
|
|
[i.relExact, t.relExact, 2000, 2, 4, 30, 60, 10],
|
|
[i.relBegins, t.relBegins, 2000, 2, 4, 30, 60, 10],
|
|
[i.relWord, t.relWord, 2000, 2, 4, 30, 60, 10],
|
|
[i.relContains, t.relContains, 2000, 2, 4, 30, 60, 10],
|
|
[i.relPublisher, t.relPublisher, 2000, 2, 4, 30, 60, 10],
|
|
...ds.mass.map(
|
|
(m) => [m.id, m.title, 2020, 2, 4, 30, 60, 10] as [string, string, number, number, number, number, number, number],
|
|
),
|
|
];
|
|
for (const g of games) {
|
|
await c.query(
|
|
`insert into public.games (id, title, visibility, year, min_players, max_players, min_minutes, max_minutes, min_age)
|
|
values ($1, $2, 'catalog', $3, $4, $5, $6, $7, $8)`,
|
|
g,
|
|
);
|
|
}
|
|
// Ausgeblendet und zusammengefuehrt: duerfen nie erscheinen (KAT-05 Kriterium 4).
|
|
await c.query(
|
|
`insert into public.games (id, title, visibility, hidden_at) values ($1, $2, 'catalog', now())`,
|
|
[i.hidden, t.hidden],
|
|
);
|
|
await c.query(
|
|
`insert into public.games (id, title, visibility, hidden_at, merged_into_id) values ($1, $2, 'catalog', now(), $3)`,
|
|
[i.merged, t.merged, i.relExact],
|
|
);
|
|
|
|
await c.query(
|
|
`insert into public.game_alt_titles (game_id, locale, title) values ($1, 'de', $2), ($1, 'en', $3)`,
|
|
[i.wald, t.waldAltDe, t.waldAltEn],
|
|
);
|
|
// Der alternative Titel des ausgeblendeten Spiels darf es nicht auffindbar machen.
|
|
await c.query(`insert into public.game_alt_titles (game_id, locale, title) values ($1, 'de', $2)`, [
|
|
i.hidden,
|
|
`${ds.W} Verstecktername`,
|
|
]);
|
|
await c.query(
|
|
`insert into public.game_publishers (game_id, publisher_id) values ($1, $2), ($3, $2), ($3, $4), ($5, $6)`,
|
|
[i.rot, i.pubQuarz, i.blau, i.pubNebel, i.relPublisher, i.pubRelevanz],
|
|
);
|
|
await c.query(
|
|
`insert into public.game_genres (game_id, genre_id) values ($1, $2), ($3, $2), ($4, $2), ($4, $5)`,
|
|
[i.familie, i.genreA, i.duo, i.experte, i.genreB],
|
|
);
|
|
await c.query(
|
|
`insert into public.game_mechanics (game_id, mechanic_id) values ($1, $3), ($2, $3)`,
|
|
[i.experte, i.duo, i.mechanicM1],
|
|
);
|
|
});
|
|
}
|
|
|
|
export async function cleanupCatalogDataset(ds: CatalogDataset): Promise<void> {
|
|
await withDb((c) => cleanupCatalog(c, ds));
|
|
}
|