import 'reflect-metadata'; import { getMetadataArgsStorage, QueryRunner } from 'typeorm'; import { Character } from '../../characters/entities/character.entity'; import { CreateVisibleVerticalSlice1787072400000 } from './1787072400000-CreateVisibleVerticalSlice'; import { Travel } from '../../travel/entities/travel.entity'; import { LocationConnection } from '../../world/entities/location-connection.entity'; import { LocationDefinition } from '../../world/entities/location-definition.entity'; describe('visible vertical slice schema', () => { it('maps the location key and relationship foreign keys explicitly', () => { const metadata = getMetadataArgsStorage(); const locationIndex = metadata.indices.find((index) => { const metadataIndex = index as typeof index & { options?: { unique?: boolean }; unique?: boolean; }; return ( index.target === LocationDefinition && index.columns?.includes('key') && (metadataIndex.options?.unique ?? metadataIndex.unique) === true ); }); expect(locationIndex).toBeDefined(); const relations = metadata.relations.filter((relation) => [Character, LocationConnection, Travel].includes( relation.target as typeof Character, ), ); const joinColumns = metadata.joinColumns .filter((joinColumn) => [Character, LocationConnection, Travel].includes( joinColumn.target as typeof Character, ), ) .map((joinColumn) => joinColumn.name); expect(joinColumns).toEqual( expect.arrayContaining([ 'current_location_id', 'from_location_id', 'to_location_id', 'character_id', 'origin_location_id', 'target_location_id', ]), ); expect( relations.map((relation) => ({ onDelete: relation.options.onDelete, propertyName: relation.propertyName, target: relation.target, })), ).toEqual( expect.arrayContaining([ expect.objectContaining({ onDelete: 'RESTRICT', propertyName: 'currentLocation', target: Character, }), expect.objectContaining({ onDelete: 'RESTRICT', propertyName: 'fromLocation', target: LocationConnection, }), expect.objectContaining({ onDelete: 'RESTRICT', propertyName: 'toLocation', target: LocationConnection, }), expect.objectContaining({ onDelete: 'RESTRICT', propertyName: 'character', target: Travel, }), expect.objectContaining({ onDelete: 'RESTRICT', propertyName: 'originLocation', target: Travel, }), expect.objectContaining({ onDelete: 'RESTRICT', propertyName: 'targetLocation', target: Travel, }), ]), ); }); it('emits reversible SQL in dependency order without extension ownership', async () => { const query = jest.fn().mockResolvedValue(undefined); const queryRunner = { query } as unknown as QueryRunner; const migration = new CreateVisibleVerticalSlice1787072400000(); await migration.up(queryRunner); const upQueries = query.mock.calls.map(([sql]) => sql as string); expect(upQueries).toHaveLength(14); expect( upQueries.map((sql) => { if (sql.includes('CREATE TABLE "location_definitions"')) { return 'create location definitions'; } if (sql.includes('IDX_location_definitions_key')) { return 'create location key index'; } if (sql.includes('CREATE TABLE "characters"')) { return 'create characters'; } if (sql.includes('IDX_characters_current_location')) { return 'create character location index'; } if (sql.includes('CREATE TABLE "location_connections"')) { return 'create location connections'; } if (sql.includes('IDX_location_connection_direction')) { return 'create connection direction index'; } if (sql.includes('IDX_location_connections_from_location')) { return 'create connection origin index'; } if (sql.includes('IDX_location_connections_to_location')) { return 'create connection target index'; } if (sql.includes('CREATE TYPE "travel_status_enum"')) { return 'create travel status enum'; } if (sql.includes('CREATE TABLE "travels"')) { return 'create travels'; } if (sql.includes('IDX_travels_character')) { return 'create travel character index'; } if (sql.includes('IDX_travels_origin_location')) { return 'create travel origin index'; } if (sql.includes('IDX_travels_target_location')) { return 'create travel target index'; } if (sql.includes('IDX_active_travel_per_character')) { return 'create active travel index'; } return 'unexpected query'; }), ).toEqual([ 'create location definitions', 'create location key index', 'create characters', 'create character location index', 'create location connections', 'create connection direction index', 'create connection origin index', 'create connection target index', 'create travel status enum', 'create travels', 'create travel character index', 'create travel origin index', 'create travel target index', 'create active travel index', ]); expect(upQueries).toEqual( expect.arrayContaining([ expect.stringContaining('CREATE TABLE "location_definitions"'), expect.stringContaining('CREATE TABLE "characters"'), expect.stringContaining('CREATE TABLE "location_connections"'), expect.stringContaining('CREATE TABLE "travels"'), expect.stringContaining( 'CREATE UNIQUE INDEX "IDX_location_connection_direction"', ), expect.stringContaining( 'CREATE UNIQUE INDEX "IDX_active_travel_per_character"', ), expect.stringContaining('WHERE "status" = \'TRAVELLING\''), ]), ); expect(upQueries[0]).toContain('CREATE TABLE "location_definitions"'); expect(upQueries).not.toEqual( expect.arrayContaining([expect.stringContaining('CREATE EXTENSION')]), ); expect(upQueries).not.toEqual( expect.arrayContaining([expect.stringContaining('uuid_generate_v4')]), ); const tableQueries = upQueries.filter((sql) => sql.includes('CREATE TABLE'), ); expect(tableQueries).toHaveLength(4); expect( tableQueries.every((sql) => sql.includes('DEFAULT gen_random_uuid()')), ).toBe(true); const foreignKeySql = upQueries .filter((sql) => sql.includes('ON DELETE RESTRICT')) .join('\n'); expect( upQueries.filter((sql) => sql.includes('ON DELETE RESTRICT')), ).toHaveLength(3); expect(foreignKeySql).toContain('FK_characters_current_location'); expect(foreignKeySql).toContain('FK_location_connections_from_location'); expect(foreignKeySql).toContain('FK_location_connections_to_location'); expect(foreignKeySql).toContain('FK_travels_character'); expect(foreignKeySql).toContain('FK_travels_origin_location'); expect(foreignKeySql).toContain('FK_travels_target_location'); await migration.down(queryRunner); const downQueries = query.mock.calls .slice(upQueries.length) .map(([sql]) => sql as string); expect(downQueries).toEqual([ 'DROP INDEX "IDX_active_travel_per_character"', 'DROP INDEX "IDX_travels_target_location"', 'DROP INDEX "IDX_travels_origin_location"', 'DROP INDEX "IDX_travels_character"', 'DROP TABLE "travels"', 'DROP INDEX "IDX_location_connections_to_location"', 'DROP INDEX "IDX_location_connections_from_location"', 'DROP INDEX "IDX_location_connection_direction"', 'DROP TABLE "location_connections"', 'DROP INDEX "IDX_characters_current_location"', 'DROP TABLE "characters"', 'DROP INDEX "IDX_location_definitions_key"', 'DROP TABLE "location_definitions"', 'DROP TYPE "travel_status_enum"', ]); }); });