feat: add versioned migrations and kysely query layer
This commit is contained in:
2
backend/libs/database/src/index.ts
Normal file
2
backend/libs/database/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './schema';
|
||||
export * from './kysely.module';
|
||||
22
backend/libs/database/src/kysely.module.ts
Normal file
22
backend/libs/database/src/kysely.module.ts
Normal 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 {}
|
||||
105
backend/libs/database/src/schema.ts
Normal file
105
backend/libs/database/src/schema.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user