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

@@ -1,8 +1,41 @@
export function runMigrations(): Promise<void> {
console.log('No migrations configured in Phase 01');
return Promise.resolve();
import { join, sep } from 'node:path';
import { runner } from 'node-pg-migrate';
import { loadEnvironment } from '../../../libs/configuration/src';
/**
* Resolves the `backend/migrations` directory relative to this file's own
* location rather than `process.cwd()`, so migrations run identically from
* the compiled `dist/apps/api/src/migration.js` (production/deploy) and from
* the TypeScript source at `apps/api/src/migration.ts` (ts-jest integration
* tests), even though those two locations sit at different directory depths
* relative to the `backend/` package root.
*/
function resolveMigrationsDir(): string {
const segments = __dirname.split(sep);
const distIndex = segments.lastIndexOf('dist');
const backendRoot =
distIndex !== -1
? segments.slice(0, distIndex).join(sep)
: segments.slice(0, -3).join(sep);
return join(backendRoot, 'migrations');
}
export async function runMigrations(): Promise<void> {
const env = loadEnvironment(process.env);
await runner({
databaseUrl: env.databaseUrl,
dir: resolveMigrationsDir(),
direction: 'up',
count: Infinity,
migrationsTable: 'pgmigrations',
checkOrder: true,
log: (message: string) => console.log(message),
});
}
if (require.main === module) {
void runMigrations();
void runMigrations().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}

View File

@@ -0,0 +1,10 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".*\\.integration-spec\\.ts$",
"testPathIgnorePatterns": ["<rootDir>/node_modules/", "<rootDir>/dist/"],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

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();
}
});
});

View File

@@ -0,0 +1,16 @@
exports.up = (pgm) => {
pgm.createExtension('pgcrypto', { ifNotExists: true });
pgm.createTable('users', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
external_subject_id: { type: 'text', notNull: true },
display_name: { type: 'text', notNull: true },
email: { type: 'text', notNull: true },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
pgm.addConstraint('users', 'users_external_subject_id_key', 'UNIQUE(external_subject_id)');
};
exports.down = (pgm) => {
pgm.dropTable('users');
};

View File

@@ -0,0 +1,24 @@
exports.up = (pgm) => {
pgm.createTable('user_preferences', {
user_id: {
type: 'uuid',
primaryKey: true,
notNull: true,
references: 'users(id)',
onDelete: 'CASCADE',
},
preferred_pace: { type: 'text' },
preferred_budget_level: { type: 'text' },
max_walking_distance_km: { type: 'numeric' },
preferred_start_time: { type: 'text' },
child_friendly_preferred: { type: 'boolean', notNull: true, default: false },
interests: { type: 'text[]', notNull: true, default: '{}' },
notes: { type: 'text' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
};
exports.down = (pgm) => {
pgm.dropTable('user_preferences');
};

View File

@@ -0,0 +1,26 @@
exports.up = (pgm) => {
pgm.createTable('trips', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
name: { type: 'text', notNull: true },
description: { type: 'text' },
owner_id: { type: 'uuid', notNull: true, references: 'users(id)' },
start_date: { type: 'date' },
end_date: { type: 'date' },
status: { type: 'text', notNull: true, default: 'DRAFT' },
planning_stage: { type: 'text' },
currency: { type: 'text', notNull: true, default: 'EUR' },
version: { type: 'integer', notNull: true, default: 1 },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
pgm.addConstraint(
'trips',
'trips_status_check',
"CHECK (status IN ('DRAFT','PLANNING','BOOKING','UPCOMING','ACTIVE','COMPLETED','ARCHIVED'))",
);
pgm.createIndex('trips', 'owner_id');
};
exports.down = (pgm) => {
pgm.dropTable('trips');
};

View File

@@ -0,0 +1,23 @@
exports.up = (pgm) => {
pgm.createTable('trip_settings', {
trip_id: {
type: 'uuid',
primaryKey: true,
notNull: true,
references: 'trips(id)',
onDelete: 'CASCADE',
},
web_research_enabled: { type: 'boolean', notNull: true, default: false },
periodic_agent_review_enabled: { type: 'boolean', notNull: true, default: false },
notification_email_enabled: { type: 'boolean', notNull: true, default: true },
notification_push_enabled: { type: 'boolean', notNull: true, default: true },
default_research_depth: { type: 'text' },
default_planning_style: { type: 'text' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
};
exports.down = (pgm) => {
pgm.dropTable('trip_settings');
};

View File

@@ -0,0 +1,25 @@
exports.up = (pgm) => {
pgm.createTable('trip_members', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
trip_id: { type: 'uuid', notNull: true, references: 'trips(id)', onDelete: 'CASCADE' },
user_id: { type: 'uuid', notNull: true, references: 'users(id)' },
role: { type: 'text', notNull: true },
status: { type: 'text', notNull: true },
joined_at: { type: 'timestamptz' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
pgm.addConstraint('trip_members', 'trip_members_trip_user_key', 'UNIQUE(trip_id, user_id)');
pgm.addConstraint('trip_members', 'trip_members_role_check', "CHECK (role IN ('OWNER','MEMBER'))");
pgm.addConstraint(
'trip_members',
'trip_members_status_check',
"CHECK (status IN ('INVITED','ACTIVE','DECLINED'))",
);
pgm.createIndex('trip_members', 'trip_id');
pgm.createIndex('trip_members', 'user_id');
};
exports.down = (pgm) => {
pgm.dropTable('trip_members');
};

View File

@@ -0,0 +1,20 @@
exports.up = (pgm) => {
pgm.createTable('trip_invitations', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
trip_id: { type: 'uuid', notNull: true, references: 'trips(id)', onDelete: 'CASCADE' },
email: { type: 'text', notNull: true },
invited_by_user_id: { type: 'uuid', notNull: true, references: 'users(id)' },
token_hash: { type: 'text', notNull: true },
expires_at: { type: 'timestamptz', notNull: true },
accepted_at: { type: 'timestamptz' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
pgm.addConstraint('trip_invitations', 'trip_invitations_token_hash_key', 'UNIQUE(token_hash)');
pgm.createIndex('trip_invitations', 'trip_id');
pgm.sql('CREATE INDEX trip_invitations_lower_email_idx ON trip_invitations (lower(email))');
};
exports.down = (pgm) => {
pgm.dropTable('trip_invitations');
};

View File

@@ -0,0 +1,22 @@
exports.up = (pgm) => {
pgm.createTable('travelers', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
trip_id: { type: 'uuid', notNull: true, references: 'trips(id)', onDelete: 'CASCADE' },
linked_user_id: { type: 'uuid', references: 'users(id)' },
display_name: { type: 'text', notNull: true },
traveler_type: { type: 'text', notNull: true },
created_by_user_id: { type: 'uuid', notNull: true, references: 'users(id)' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
pgm.addConstraint(
'travelers',
'travelers_traveler_type_check',
"CHECK (traveler_type IN ('ADULT','CHILD','INFANT'))",
);
pgm.createIndex('travelers', 'trip_id');
};
exports.down = (pgm) => {
pgm.dropTable('travelers');
};

View File

@@ -0,0 +1,30 @@
exports.up = (pgm) => {
pgm.createTable('trip_preference_overrides', {
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
trip_id: { type: 'uuid', notNull: true, references: 'trips(id)', onDelete: 'CASCADE' },
user_id: { type: 'uuid', references: 'users(id)' },
traveler_id: { type: 'uuid', references: 'travelers(id)', onDelete: 'CASCADE' },
overrides: { type: 'jsonb', notNull: true, default: '{}' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
});
pgm.addConstraint(
'trip_preference_overrides',
'trip_preference_overrides_subject_check',
'CHECK (num_nonnulls(user_id, traveler_id) = 1)',
);
pgm.createIndex('trip_preference_overrides', ['trip_id', 'user_id'], {
unique: true,
where: 'user_id IS NOT NULL',
name: 'trip_pref_overrides_trip_user_uq',
});
pgm.createIndex('trip_preference_overrides', ['trip_id', 'traveler_id'], {
unique: true,
where: 'traveler_id IS NOT NULL',
name: 'trip_pref_overrides_trip_traveler_uq',
});
};
exports.down = (pgm) => {
pgm.dropTable('trip_preference_overrides');
};

View File

@@ -20,13 +20,16 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./apps/api/test/jest-e2e.json"
"test:e2e": "jest --config ./apps/api/test/jest-e2e.json",
"test:integration": "jest --config jest-integration.json --runInBand"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"ioredis": "^6.0.0",
"kysely": "^0.29.5",
"node-pg-migrate": "^7.9.1",
"pg": "^8.23.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"

View File

@@ -15,6 +15,7 @@ COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/backend/node_modules ./backend/node_modules
COPY --from=build /app/backend/package.json ./backend/package.json
COPY --from=build /app/backend/dist ./backend/dist
COPY --from=build /app/backend/migrations ./backend/migrations
USER node
EXPOSE 3000
CMD ["node", "backend/dist/apps/api/src/main.js"]

160
pnpm-lock.yaml generated
View File

@@ -26,6 +26,12 @@ importers:
ioredis:
specifier: ^6.0.0
version: 6.0.0
kysely:
specifier: ^0.29.5
version: 0.29.5
node-pg-migrate:
specifier: ^7.9.1
version: 7.9.1(@types/pg@8.21.0)(pg@8.23.0)
pg:
specifier: ^8.23.0
version: 8.23.0
@@ -68,13 +74,13 @@ importers:
version: 7.2.1
eslint:
specifier: ^9.18.0
version: 9.39.5
version: 9.39.5(jiti@2.7.0)
eslint-config-prettier:
specifier: ^10.0.1
version: 10.1.8(eslint@9.39.5)
version: 10.1.8(eslint@9.39.5(jiti@2.7.0))
eslint-plugin-prettier:
specifier: ^5.2.2
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.5))(eslint@9.39.5)(prettier@3.9.6)
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6)
globals:
specifier: ^17.0.0
version: 17.11.0
@@ -107,7 +113,7 @@ importers:
version: 5.9.3
typescript-eslint:
specifier: ^8.20.0
version: 8.67.0(eslint@9.39.5)(typescript@5.9.3)
version: 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
frontend:
dependencies:
@@ -141,7 +147,7 @@ importers:
devDependencies:
'@angular/build':
specifier: ^21.2.6
version: 21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)))(yaml@2.9.0)
version: 21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(jiti@2.7.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)))(yaml@2.9.0)
'@angular/cli':
specifier: ^21.2.6
version: 21.2.21(@types/node@24.13.3)(chokidar@5.0.0)
@@ -159,7 +165,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.8
version: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
version: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
packages:
@@ -1031,6 +1037,10 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/cliui@9.0.0':
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@@ -3085,6 +3095,12 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@11.0.3:
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
@@ -3307,6 +3323,10 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.2.3:
resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
engines: {node: 20 || >=22}
jest-changed-files@30.4.1:
resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -3439,6 +3459,10 @@ packages:
node-notifier:
optional: true
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
jose@6.2.9:
resolution: {integrity: sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==}
@@ -3507,6 +3531,10 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kysely@0.29.5:
resolution: {integrity: sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ==}
engines: {node: '>=22.0.0'}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -3764,6 +3792,17 @@ packages:
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
node-pg-migrate@7.9.1:
resolution: {integrity: sha512-6z4OSN27ye8aYdX9ZU7NN2PTI5pOp34hTr+22Ej12djIYECq++gT7LPLZVOQXEeVCBOZQLqf87kC3Y36G434OQ==}
engines: {node: '>=18.19.0'}
hasBin: true
peerDependencies:
'@types/pg': '>=6.0.0 <9.0.0'
pg: '>=4.3.0 <9.0.0'
peerDependenciesMeta:
'@types/pg':
optional: true
node-releases@2.0.53:
resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
engines: {node: '>=18'}
@@ -5115,7 +5154,7 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@angular/build@21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)))(yaml@2.9.0)':
'@angular/build@21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(jiti@2.7.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)))(yaml@2.9.0)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.21(chokidar@5.0.0)
@@ -5125,7 +5164,7 @@ snapshots:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.21(@types/node@24.13.3)
'@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
'@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
beasties: 0.4.1
browserslist: 4.28.8
esbuild: 0.28.1
@@ -5146,7 +5185,7 @@ snapshots:
tslib: 2.8.1
typescript: 5.9.3
undici: 7.29.0
vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
watchpack: 2.5.1
optionalDependencies:
'@angular/core': 21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)
@@ -5154,7 +5193,7 @@ snapshots:
'@angular/service-worker': 21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2)
lmdb: 3.5.1
postcss: 8.5.26
vitest: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
vitest: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -5606,9 +5645,9 @@ snapshots:
'@esbuild/win32-x64@0.28.1':
optional: true
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
'@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))':
dependencies:
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -5832,6 +5871,8 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/cliui@9.0.0': {}
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.3
@@ -6756,15 +6797,15 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5)(typescript@5.9.3)
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/type-utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3)
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3)
'@typescript-eslint/type-utils': 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.67.0
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
ignore: 7.0.6
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -6772,14 +6813,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3)':
'@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.67.0
debug: 4.4.3
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -6802,13 +6843,13 @@ snapshots:
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.67.0(eslint@9.39.5)(typescript@5.9.3)':
'@typescript-eslint/type-utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3)
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
@@ -6831,13 +6872,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.67.0(eslint@9.39.5)(typescript@5.9.3)':
'@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -6919,9 +6960,9 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true
'@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))':
'@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))':
dependencies:
vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
'@vitest/expect@4.1.10':
dependencies:
@@ -6932,13 +6973,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.1
'@vitest/mocker@4.1.10(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))':
'@vitest/mocker@4.1.10(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.10':
dependencies:
@@ -7668,19 +7709,19 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@9.39.5):
eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)):
dependencies:
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.5))(eslint@9.39.5)(prettier@3.9.6):
eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6):
dependencies:
eslint: 9.39.5
eslint: 9.39.5(jiti@2.7.0)
prettier: 3.9.6
prettier-linter-helpers: 1.0.1
synckit: 0.11.13
optionalDependencies:
'@types/eslint': 9.6.1
eslint-config-prettier: 10.1.8(eslint@9.39.5)
eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0))
eslint-scope@5.1.1:
dependencies:
@@ -7698,9 +7739,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
eslint@9.39.5:
eslint@9.39.5(jiti@2.7.0):
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
@@ -7734,6 +7775,8 @@ snapshots:
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 2.7.0
transitivePeerDependencies:
- supports-color
@@ -8010,6 +8053,15 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@11.0.3:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.2.3
minimatch: 10.2.6
minipass: 7.1.3
package-json-from-dist: 1.0.1
path-scurry: 2.0.2
glob@13.0.6:
dependencies:
minimatch: 10.2.6
@@ -8226,6 +8278,10 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@4.2.3:
dependencies:
'@isaacs/cliui': 9.0.0
jest-changed-files@30.4.1:
dependencies:
execa: 5.1.1
@@ -8544,6 +8600,9 @@ snapshots:
- supports-color
- ts-node
jiti@2.7.0:
optional: true
jose@6.2.9: {}
js-tokens@4.0.0: {}
@@ -8616,6 +8675,8 @@ snapshots:
dependencies:
json-buffer: 3.0.1
kysely@0.29.5: {}
leven@3.1.0: {}
levn@0.4.1:
@@ -8887,6 +8948,14 @@ snapshots:
node-int64@0.4.0: {}
node-pg-migrate@7.9.1(@types/pg@8.21.0)(pg@8.23.0):
dependencies:
glob: 11.0.3
pg: 8.23.0
yargs: 17.7.3
optionalDependencies:
'@types/pg': 8.21.0
node-releases@2.0.53: {}
nopt@9.0.0:
@@ -9776,13 +9845,13 @@ snapshots:
typedarray@0.0.6: {}
typescript-eslint@8.67.0(eslint@9.39.5)(typescript@5.9.3):
typescript-eslint@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5)(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5)(typescript@5.9.3)
eslint: 9.39.5
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
eslint: 9.39.5(jiti@2.7.0)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -9859,7 +9928,7 @@ snapshots:
vary@1.1.2: {}
vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0):
vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0):
dependencies:
esbuild: 0.28.1
fdir: 6.5.0(picomatch@4.0.4)
@@ -9870,14 +9939,15 @@ snapshots:
optionalDependencies:
'@types/node': 24.13.3
fsevents: 2.3.3
jiti: 2.7.0
sass: 1.97.3
terser: 5.50.0
yaml: 2.9.0
vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)):
vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
'@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
@@ -9894,7 +9964,7 @@ snapshots:
tinyexec: 1.3.0
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.13.3