feat: add versioned migrations and kysely query layer

This commit is contained in:
Bastian Wagner
2026-08-17 14:33:28 +02:00
parent acfbb3ab22
commit ec95a8e527
17 changed files with 513 additions and 50 deletions

View File

@@ -0,0 +1,2 @@
export * from './schema';
export * from './kysely.module';

View File

@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { Kysely, PostgresDialect } from 'kysely';
import type { Pool } from 'pg';
import { PostgresModule } from '../../infrastructure/src';
import { POSTGRES_POOL } from '../../infrastructure/src';
import type { Database } from './schema';
export const KYSELY_DB = Symbol('KYSELY_DB');
export const kyselyDbProvider = {
provide: KYSELY_DB,
inject: [POSTGRES_POOL],
useFactory: (pool: Pool) =>
new Kysely<Database>({ dialect: new PostgresDialect({ pool }) }),
};
@Module({
imports: [PostgresModule],
providers: [kyselyDbProvider],
exports: [kyselyDbProvider],
})
export class DatabaseModule {}

View File

@@ -0,0 +1,105 @@
import type { ColumnType, Generated } from 'kysely';
export interface UsersTable {
id: Generated<string>;
external_subject_id: string;
display_name: string;
email: string;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface UserPreferencesTable {
user_id: string;
preferred_pace: string | null;
preferred_budget_level: string | null;
max_walking_distance_km: number | null;
preferred_start_time: string | null;
child_friendly_preferred: boolean;
interests: string[];
notes: string | null;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface TripsTable {
id: Generated<string>;
name: string;
description: string | null;
owner_id: string;
start_date: string | null;
end_date: string | null;
status: string;
planning_stage: string | null;
currency: string;
version: Generated<number>;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface TripSettingsTable {
trip_id: string;
web_research_enabled: boolean;
periodic_agent_review_enabled: boolean;
notification_email_enabled: boolean;
notification_push_enabled: boolean;
default_research_depth: string | null;
default_planning_style: string | null;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface TripMembersTable {
id: Generated<string>;
trip_id: string;
user_id: string;
role: string;
status: string;
joined_at: ColumnType<Date, string | undefined, string> | null;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface TripInvitationsTable {
id: Generated<string>;
trip_id: string;
email: string;
invited_by_user_id: string;
token_hash: string;
expires_at: ColumnType<Date, string, string>;
accepted_at: ColumnType<Date, string | undefined, string> | null;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface TravelersTable {
id: Generated<string>;
trip_id: string;
linked_user_id: string | null;
display_name: string;
traveler_type: string;
created_by_user_id: string;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface TripPreferenceOverridesTable {
id: Generated<string>;
trip_id: string;
user_id: string | null;
traveler_id: string | null;
overrides: unknown;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, string>;
}
export interface Database {
users: UsersTable;
user_preferences: UserPreferencesTable;
trips: TripsTable;
trip_settings: TripSettingsTable;
trip_members: TripMembersTable;
trip_invitations: TripInvitationsTable;
travelers: TravelersTable;
trip_preference_overrides: TripPreferenceOverridesTable;
}

View File

@@ -0,0 +1,31 @@
import { Pool } from 'pg';
import { runMigrations } from '../../../apps/api/src/migration';
describe('runMigrations (integration)', () => {
it('applies all pending migrations idempotently against a real database', async () => {
await runMigrations();
await runMigrations(); // must be safe to run twice
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
try {
const tables = await pool.query<{ table_name: string }>(
`select table_name from information_schema.tables where table_schema = 'public'`,
);
const names = tables.rows.map((r) => r.table_name);
for (const expected of [
'users',
'user_preferences',
'trips',
'trip_settings',
'trip_members',
'trip_invitations',
'travelers',
'trip_preference_overrides',
]) {
expect(names).toContain(expected);
}
} finally {
await pool.end();
}
});
});