fix: preserve seeded location identities

This commit is contained in:
Bastian Wagner
2026-08-18 19:40:13 +02:00
parent 067134b684
commit c35859a4de
2 changed files with 159 additions and 57 deletions

View File

@@ -1,4 +1,7 @@
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { Character } from '../../characters/entities/character.entity';
import { LocationConnection } from '../../world/entities/location-connection.entity';
import { LocationDefinition } from '../../world/entities/location-definition.entity';
import { seedVisibleVerticalSlice } from './vertical-slice.seed'; import { seedVisibleVerticalSlice } from './vertical-slice.seed';
type Row = Record<string, unknown>; type Row = Record<string, unknown>;
@@ -39,6 +42,41 @@ class InMemoryRepository {
readonly insert = jest.fn(async (value: Row) => { readonly insert = jest.fn(async (value: Row) => {
this.rows.push({ ...value }); this.rows.push({ ...value });
}); });
readonly update = jest.fn(async (criteria: string | Row, value: Row) => {
const row = this.rows.find((candidate) =>
typeof criteria === 'string'
? candidate.id === criteria
: Object.entries(criteria).every(
([key, expected]) => candidate[key] === expected,
),
);
if (row) {
Object.assign(row, value);
}
});
}
function createDataSource(
locationRepository: InMemoryRepository,
connectionRepository: InMemoryRepository,
characterRepository: InMemoryRepository,
): DataSource {
return {
getRepository: jest.fn((entity: unknown) => {
if (entity === LocationDefinition) {
return locationRepository;
}
if (entity === LocationConnection) {
return connectionRepository;
}
if (entity === Character) {
return characterRepository;
}
throw new Error('Unexpected repository');
}),
} as unknown as DataSource;
} }
describe('seedVisibleVerticalSlice', () => { describe('seedVisibleVerticalSlice', () => {
@@ -46,27 +84,20 @@ describe('seedVisibleVerticalSlice', () => {
const locationRepository = new InMemoryRepository(); const locationRepository = new InMemoryRepository();
const connectionRepository = new InMemoryRepository(); const connectionRepository = new InMemoryRepository();
const characterRepository = new InMemoryRepository(); const characterRepository = new InMemoryRepository();
const dataSource = { const dataSource = createDataSource(
getRepository: jest locationRepository,
.fn() connectionRepository,
.mockReturnValueOnce(locationRepository) characterRepository,
.mockReturnValueOnce(connectionRepository)
.mockReturnValueOnce(characterRepository)
.mockReturnValueOnce(locationRepository)
.mockReturnValueOnce(connectionRepository)
.mockReturnValueOnce(characterRepository),
} as unknown as DataSource;
await seedVisibleVerticalSlice(dataSource);
await seedVisibleVerticalSlice(dataSource);
expect(locationRepository.upsert).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ id: SOUTH_GATE_ID, key: 'south-gate' }),
expect.objectContaining({ id: BURNED_ROAD_ID, key: 'burned-road' }),
]),
['key'],
); );
await seedVisibleVerticalSlice(dataSource);
Object.assign(characterRepository.rows[0], {
currentLocationId: BURNED_ROAD_ID,
currentHp: 57,
experience: 39,
});
await seedVisibleVerticalSlice(dataSource);
expect(connectionRepository.upsert).toHaveBeenCalledWith( expect(connectionRepository.upsert).toHaveBeenCalledWith(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -86,5 +117,57 @@ describe('seedVisibleVerticalSlice', () => {
expect(locationRepository.rows).toHaveLength(2); expect(locationRepository.rows).toHaveLength(2);
expect(connectionRepository.rows).toHaveLength(2); expect(connectionRepository.rows).toHaveLength(2);
expect(characterRepository.rows).toHaveLength(1); expect(characterRepository.rows).toHaveLength(1);
expect(characterRepository.rows[0]).toEqual(
expect.objectContaining({
currentLocationId: BURNED_ROAD_ID,
currentHp: 57,
experience: 39,
}),
);
});
it('preserves existing location IDs and uses them for the directed connections', async () => {
const locationRepository = new InMemoryRepository();
const connectionRepository = new InMemoryRepository();
const characterRepository = new InMemoryRepository();
const persistedSouthGateId = '30000000-0000-4000-8000-000000000001';
locationRepository.rows.push({
id: persistedSouthGateId,
key: 'south-gate',
name: 'Veraltetes Südtor',
});
const dataSource = createDataSource(
locationRepository,
connectionRepository,
characterRepository,
);
await seedVisibleVerticalSlice(dataSource);
expect(locationRepository.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: persistedSouthGateId,
key: 'south-gate',
name: 'Südtor von Graufurt',
}),
expect.objectContaining({
id: BURNED_ROAD_ID,
key: 'burned-road',
}),
]),
);
expect(connectionRepository.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
fromLocationId: persistedSouthGateId,
toLocationId: BURNED_ROAD_ID,
}),
expect.objectContaining({
fromLocationId: BURNED_ROAD_ID,
toLocationId: persistedSouthGateId,
}),
]),
);
}); });
}); });

View File

@@ -12,52 +12,71 @@ export async function seedVisibleVerticalSlice(
const connectionRepository = dataSource.getRepository(LocationConnection); const connectionRepository = dataSource.getRepository(LocationConnection);
const characterRepository = dataSource.getRepository(Character); const characterRepository = dataSource.getRepository(Character);
await locationRepository.upsert( const locations = [
[ {
{ id: SOUTH_GATE_ID,
id: SOUTH_GATE_ID, key: 'south-gate',
key: 'south-gate', name: 'Südtor von Graufurt',
name: 'Südtor von Graufurt', description:
description: 'Am schwarzen Südtor endet der Schutz Graufurts. Hinter den Wachtfeuern beginnt die stille Weite der Aschenfelder.',
'Am schwarzen Südtor endet der Schutz Graufurts. Hinter den Wachtfeuern beginnt die stille Weite der Aschenfelder.', regionKey: 'ashen-fields',
regionKey: 'ashen-fields', minRecommendedLevel: 1,
minRecommendedLevel: 1, maxRecommendedLevel: 1,
maxRecommendedLevel: 1, dangerLevel: 0,
dangerLevel: 0, isSafe: true,
isSafe: true, huntingEnabled: false,
huntingEnabled: false, artworkPath: '/assets/locations/south-gate.webp',
artworkPath: '/assets/locations/south-gate.webp', },
}, {
{ id: BURNED_ROAD_ID,
id: BURNED_ROAD_ID, key: 'burned-road',
key: 'burned-road', name: 'Verbrannte Straße',
name: 'Verbrannte Straße', description:
description: 'Die alte Handelsstraße führt durch verkohlte Felder. Zwischen Asche und zerbrochenen Wagen warten die ersten Gefahren.',
'Die alte Handelsstraße führt durch verkohlte Felder. Zwischen Asche und zerbrochenen Wagen warten die ersten Gefahren.', regionKey: 'ashen-fields',
regionKey: 'ashen-fields', minRecommendedLevel: 1,
minRecommendedLevel: 1, maxRecommendedLevel: 2,
maxRecommendedLevel: 2, dangerLevel: 1,
dangerLevel: 1, isSafe: false,
isSafe: false, huntingEnabled: true,
huntingEnabled: true, artworkPath: '/assets/locations/burned-road.webp',
artworkPath: '/assets/locations/burned-road.webp', },
}, ];
], let southGateId = SOUTH_GATE_ID;
['key'], let burnedRoadId = BURNED_ROAD_ID;
);
for (const location of locations) {
const existing = await locationRepository.findOneBy({
key: location.key,
});
const { id, key, ...definition } = location;
const persistedId = existing?.id ?? id;
if (existing) {
await locationRepository.update(existing.id, definition);
} else {
await locationRepository.insert(location);
}
if (key === 'south-gate') {
southGateId = persistedId;
} else {
burnedRoadId = persistedId;
}
}
await connectionRepository.upsert( await connectionRepository.upsert(
[ [
{ {
fromLocationId: SOUTH_GATE_ID, fromLocationId: southGateId,
toLocationId: BURNED_ROAD_ID, toLocationId: burnedRoadId,
travelDurationSeconds: 10, travelDurationSeconds: 10,
ambushChance: '0.0500', ambushChance: '0.0500',
enabled: true, enabled: true,
}, },
{ {
fromLocationId: BURNED_ROAD_ID, fromLocationId: burnedRoadId,
toLocationId: SOUTH_GATE_ID, toLocationId: southGateId,
travelDurationSeconds: 10, travelDurationSeconds: 10,
ambushChance: '0.0500', ambushChance: '0.0500',
enabled: true, enabled: true,