feat: kysely migrations (#17198)

This commit is contained in:
Jason Rasmussen 2025-03-29 09:26:24 -04:00 committed by GitHub
parent 6fa0cb534a
commit 55a3c30664
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 267 additions and 126 deletions

View File

@ -4,8 +4,8 @@ process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich';
import { writeFileSync } from 'node:fs';
import postgres from 'postgres';
import { ConfigRepository } from 'src/repositories/config.repository';
import 'src/schema/tables';
import { DatabaseTable, schemaDiff, schemaFromDatabase, schemaFromDecorators } from 'src/sql-tools';
import 'src/tables';
const main = async () => {
const command = process.argv[2];
@ -54,9 +54,10 @@ const generate = async (name: string) => {
};
const create = (name: string, up: string[], down: string[]) => {
const { filename, code } = asMigration(name, up, down);
const timestamp = Date.now();
const filename = `${timestamp}-${name}.ts`;
const fullPath = `./src/${filename}`;
writeFileSync(fullPath, code);
writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down }));
console.log(`Wrote ${fullPath}`);
};
@ -79,14 +80,21 @@ const compare = async () => {
return { up, down };
};
const asMigration = (name: string, up: string[], down: string[]) => {
const timestamp = Date.now();
type MigrationProps = {
name: string;
timestamp: number;
up: string[];
down: string[];
};
const asMigration = (type: 'kysely' | 'typeorm', options: MigrationProps) =>
type === 'typeorm' ? asTypeOrmMigration(options) : asKyselyMigration(options);
const asTypeOrmMigration = ({ timestamp, name, up, down }: MigrationProps) => {
const upSql = up.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n');
const downSql = down.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n');
return {
filename: `${timestamp}-${name}.ts`,
code: `import { MigrationInterface, QueryRunner } from 'typeorm';
return `import { MigrationInterface, QueryRunner } from 'typeorm';
export class ${name}${timestamp} implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
@ -97,8 +105,23 @@ ${upSql}
${downSql}
}
}
`,
};
`;
};
const asKyselyMigration = ({ up, down }: MigrationProps) => {
const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
return `import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
${upSql}
}
export async function down(db: Kysely<any>): Promise<void> {
${downSql}
}
`;
};
main()

2
server/src/db.d.ts vendored
View File

@ -5,7 +5,7 @@
import type { ColumnType } from 'kysely';
import { AssetType, MemoryType, Permission, SyncEntityType } from 'src/enum';
import { UserTable } from 'src/tables/user.table';
import { UserTable } from 'src/schema/tables/user.table';
import { OnThisDayData } from 'src/types';
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;

View File

@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock';
import { Kysely, sql, Transaction } from 'kysely';
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { mkdir, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import semver from 'semver';
import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
import { DB } from 'src/db';
@ -200,9 +202,49 @@ export class DatabaseRepository {
this.logger.log('Running migrations, this may take a while');
this.logger.debug('Running typeorm migrations');
await dataSource.initialize();
await dataSource.runMigrations(options);
await dataSource.destroy();
this.logger.debug('Finished running typeorm migrations');
// eslint-disable-next-line unicorn/prefer-module
const migrationFolder = join(__dirname, '..', 'schema/migrations');
// TODO remove after we have at least one kysely migration
await mkdir(migrationFolder, { recursive: true });
this.logger.debug('Running kysely migrations');
const migrator = new Migrator({
db: this.db,
migrationLockTableName: 'kysely_migrations_lock',
migrationTableName: 'kysely_migrations',
provider: new FileMigrationProvider({
fs: { readdir },
path: { join },
migrationFolder,
}),
});
const { error, results } = await migrator.migrateToLatest();
for (const result of results ?? []) {
if (result.status === 'Success') {
this.logger.log(`Migration "${result.migrationName}" succeeded`);
}
if (result.status === 'Error') {
this.logger.warn(`Migration "${result.migrationName}" failed`);
}
}
if (error) {
this.logger.error(`Kysely migrations failed: ${error}`);
throw error;
}
this.logger.debug('Finished running kysely migrations');
}
async withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R> {

View File

@ -8,7 +8,7 @@ import { DummyValue, GenerateSql } from 'src/decorators';
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserEntity, withMetadata } from 'src/entities/user.entity';
import { AssetType, UserStatus } from 'src/enum';
import { UserTable } from 'src/tables/user.table';
import { UserTable } from 'src/schema/tables/user.table';
import { asUuid } from 'src/utils/database';
type Upsert = Insertable<DbUserMetadata>;

View File

@ -1,3 +1,6 @@
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Check,
Column,
@ -10,9 +13,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { AlbumTable } from 'src/tables/album.table';
import { AssetTable } from 'src/tables/asset.table';
import { UserTable } from 'src/tables/user.table';
@Table('activity')
@Index({

View File

@ -1,6 +1,6 @@
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AlbumTable } from 'src/tables/album.table';
import { AssetTable } from 'src/tables/asset.table';
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
export class AlbumAssetTable {

View File

@ -1,7 +1,7 @@
import { AlbumUserRole } from 'src/enum';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
import { AlbumTable } from 'src/tables/album.table';
import { UserTable } from 'src/tables/user.table';
@Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' })
// Pre-existing indices from original album <--> user ManyToMany mapping

View File

@ -1,4 +1,6 @@
import { AssetOrder } from 'src/enum';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -10,8 +12,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { UserTable } from 'src/tables/user.table';
@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' })
export class AlbumTable {

View File

@ -1,4 +1,5 @@
import { Permission } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -9,7 +10,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('api_keys')
export class APIKeyTable {

View File

@ -1,7 +1,7 @@
import { SourceType } from 'src/enum';
import { AssetTable } from 'src/schema/tables/asset.table';
import { PersonTable } from 'src/schema/tables/person.table';
import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { PersonTable } from 'src/tables/person.table';
@Table({ name: 'asset_faces' })
@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] })

View File

@ -1,5 +1,5 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
@Table('asset_job_status')
export class AssetJobStatusTable {

View File

@ -1,5 +1,8 @@
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum';
import { LibraryTable } from 'src/schema/tables/library.table';
import { StackTable } from 'src/schema/tables/stack.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -12,9 +15,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { LibraryTable } from 'src/tables/library.table';
import { StackTable } from 'src/tables/stack.table';
import { UserTable } from 'src/tables/user.table';
@Table('assets')
// Checksums must be unique per user and library

View File

@ -1,5 +1,5 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn, UpdateIdColumn } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
@Table('exif')
export class ExifTable {

View File

@ -1,5 +1,5 @@
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetFaceTable } from 'src/tables/asset-face.table';
@Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' })
export class FaceSearchTable {

View File

@ -0,0 +1,73 @@
import { ActivityTable } from 'src/schema/tables/activity.table';
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
import { AlbumTable } from 'src/schema/tables/album.table';
import { APIKeyTable } from 'src/schema/tables/api-key.table';
import { AssetAuditTable } from 'src/schema/tables/asset-audit.table';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { AuditTable } from 'src/schema/tables/audit.table';
import { ExifTable } from 'src/schema/tables/exif.table';
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
import { LibraryTable } from 'src/schema/tables/library.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
import { MoveTable } from 'src/schema/tables/move.table';
import {
NaturalEarthCountriesTable,
NaturalEarthCountriesTempTable,
} from 'src/schema/tables/natural-earth-countries.table';
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
import { PartnerTable } from 'src/schema/tables/partner.table';
import { PersonTable } from 'src/schema/tables/person.table';
import { SessionTable } from 'src/schema/tables/session.table';
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
import { SmartSearchTable } from 'src/schema/tables/smart-search.table';
import { StackTable } from 'src/schema/tables/stack.table';
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
import { SystemMetadataTable } from 'src/schema/tables/system-metadata.table';
import { TagAssetTable } from 'src/schema/tables/tag-asset.table';
import { UserAuditTable } from 'src/schema/tables/user-audit.table';
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
import { UserTable } from 'src/schema/tables/user.table';
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
export const tables = [
ActivityTable,
AlbumAssetTable,
AlbumUserTable,
AlbumTable,
APIKeyTable,
AssetAuditTable,
AssetFaceTable,
AssetJobStatusTable,
AssetTable,
AuditTable,
ExifTable,
FaceSearchTable,
GeodataPlacesTable,
LibraryTable,
MemoryAssetTable,
MemoryTable,
MoveTable,
NaturalEarthCountriesTable,
NaturalEarthCountriesTempTable,
PartnerAuditTable,
PartnerTable,
PersonTable,
SessionTable,
SharedLinkAssetTable,
SharedLinkTable,
SmartSearchTable,
StackTable,
SessionSyncCheckpointTable,
SystemMetadataTable,
TagAssetTable,
UserAuditTable,
UserMetadataTable,
UserTable,
VersionHistoryTable,
];

View File

@ -1,3 +1,4 @@
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -9,7 +10,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('libraries')
export class LibraryTable {

View File

@ -1,4 +1,5 @@
import { MemoryType } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -10,7 +11,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
import { MemoryData } from 'src/types';
@Table('memories')

View File

@ -1,6 +1,6 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { MemoryTable } from 'src/tables/memory.table';
@Table('memories_assets_assets')
export class MemoryAssetTable {

View File

@ -1,3 +1,4 @@
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -7,7 +8,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('partners')
export class PartnerTable {

View File

@ -1,3 +1,5 @@
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Check,
Column,
@ -9,8 +11,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { AssetFaceTable } from 'src/tables/asset-face.table';
import { UserTable } from 'src/tables/user.table';
@Table('person')
@Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` })

View File

@ -1,3 +1,4 @@
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -8,7 +9,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table({ name: 'sessions', primaryConstraintName: 'PK_48cb6b5c20faa63157b3c1baf7f' })
export class SessionTable {

View File

@ -1,6 +1,6 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { SharedLinkTable } from 'src/tables/shared-link.table';
@Table('shared_link__asset')
export class SharedLinkAssetTable {

View File

@ -1,4 +1,6 @@
import { SharedLinkType } from 'src/enum';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -8,8 +10,6 @@ import {
Table,
Unique,
} from 'src/sql-tools';
import { AlbumTable } from 'src/tables/album.table';
import { UserTable } from 'src/tables/user.table';
@Table('shared_links')
@Unique({ name: 'UQ_sharedlink_key', columns: ['key'] })

View File

@ -1,5 +1,5 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
@Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' })
export class SmartSearchTable {

View File

@ -1,6 +1,6 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import { ForeignKeyColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { UserTable } from 'src/tables/user.table';
@Table('asset_stack')
export class StackTable {

View File

@ -1,4 +1,5 @@
import { SyncEntityType } from 'src/enum';
import { SessionTable } from 'src/schema/tables/session.table';
import {
Column,
ColumnIndex,
@ -9,7 +10,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { SessionTable } from 'src/tables/session.table';
@Table('session_sync_checkpoints')
export class SessionSyncCheckpointTable {

View File

@ -1,6 +1,6 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { TagTable } from 'src/schema/tables/tag.table';
import { ColumnIndex, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { TagTable } from 'src/tables/tag.table';
@Index({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] })
@Table('tag_asset')

View File

@ -1,5 +1,5 @@
import { TagTable } from 'src/schema/tables/tag.table';
import { ColumnIndex, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools';
import { TagTable } from 'src/tables/tag.table';
@Table('tags_closure')
export class TagClosureTable {

View File

@ -1,3 +1,4 @@
import { UserTable } from 'src/schema/tables/user.table';
import {
Column,
ColumnIndex,
@ -9,7 +10,6 @@ import {
UpdateDateColumn,
UpdateIdColumn,
} from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('tags')
@Unique({ columns: ['userId', 'value'] })

View File

@ -1,7 +1,7 @@
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserMetadataKey } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('user_metadata')
export class UserMetadataTable<T extends keyof UserMetadata = UserMetadataKey> implements UserMetadataItem<T> {

View File

@ -46,7 +46,7 @@ import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
import { UserTable } from 'src/tables/user.table';
import { UserTable } from 'src/schema/tables/user.table';
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
import { getConfig, updateConfig } from 'src/utils/config';

View File

@ -1,70 +0,0 @@
import { ActivityTable } from 'src/tables/activity.table';
import { AlbumAssetTable } from 'src/tables/album-asset.table';
import { AlbumUserTable } from 'src/tables/album-user.table';
import { AlbumTable } from 'src/tables/album.table';
import { APIKeyTable } from 'src/tables/api-key.table';
import { AssetAuditTable } from 'src/tables/asset-audit.table';
import { AssetFaceTable } from 'src/tables/asset-face.table';
import { AssetJobStatusTable } from 'src/tables/asset-job-status.table';
import { AssetTable } from 'src/tables/asset.table';
import { AuditTable } from 'src/tables/audit.table';
import { ExifTable } from 'src/tables/exif.table';
import { FaceSearchTable } from 'src/tables/face-search.table';
import { GeodataPlacesTable } from 'src/tables/geodata-places.table';
import { LibraryTable } from 'src/tables/library.table';
import { MemoryTable } from 'src/tables/memory.table';
import { MemoryAssetTable } from 'src/tables/memory_asset.table';
import { MoveTable } from 'src/tables/move.table';
import { NaturalEarthCountriesTable, NaturalEarthCountriesTempTable } from 'src/tables/natural-earth-countries.table';
import { PartnerAuditTable } from 'src/tables/partner-audit.table';
import { PartnerTable } from 'src/tables/partner.table';
import { PersonTable } from 'src/tables/person.table';
import { SessionTable } from 'src/tables/session.table';
import { SharedLinkAssetTable } from 'src/tables/shared-link-asset.table';
import { SharedLinkTable } from 'src/tables/shared-link.table';
import { SmartSearchTable } from 'src/tables/smart-search.table';
import { StackTable } from 'src/tables/stack.table';
import { SessionSyncCheckpointTable } from 'src/tables/sync-checkpoint.table';
import { SystemMetadataTable } from 'src/tables/system-metadata.table';
import { TagAssetTable } from 'src/tables/tag-asset.table';
import { UserAuditTable } from 'src/tables/user-audit.table';
import { UserMetadataTable } from 'src/tables/user-metadata.table';
import { UserTable } from 'src/tables/user.table';
import { VersionHistoryTable } from 'src/tables/version-history.table';
export const tables = [
ActivityTable,
AlbumAssetTable,
AlbumUserTable,
AlbumTable,
APIKeyTable,
AssetAuditTable,
AssetFaceTable,
AssetJobStatusTable,
AssetTable,
AuditTable,
ExifTable,
FaceSearchTable,
GeodataPlacesTable,
LibraryTable,
MemoryAssetTable,
MemoryTable,
MoveTable,
NaturalEarthCountriesTable,
NaturalEarthCountriesTempTable,
PartnerAuditTable,
PartnerTable,
PersonTable,
SessionTable,
SharedLinkAssetTable,
SharedLinkTable,
SmartSearchTable,
StackTable,
SessionSyncCheckpointTable,
SystemMetadataTable,
TagAssetTable,
UserAuditTable,
UserMetadataTable,
UserTable,
VersionHistoryTable,
];

View File

@ -35,7 +35,7 @@ import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
import { UserTable } from 'src/tables/user.table';
import { UserTable } from 'src/schema/tables/user.table';
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { newUuid } from 'test/small.factory';
import { automock } from 'test/utils';

View File

@ -1,8 +1,14 @@
import { FileMigrationProvider, Kysely, Migrator } from 'kysely';
import { PostgresJSDialect } from 'kysely-postgres-js';
import { mkdir, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { parse } from 'pg-connection-string';
import postgres, { Notice } from 'postgres';
import { GenericContainer, Wait } from 'testcontainers';
import { DataSource } from 'typeorm';
const globalSetup = async () => {
const postgres = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
const postgresContainer = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
.withExposedPorts(5432)
.withEnvironment({
POSTGRES_PASSWORD: 'postgres',
@ -29,7 +35,7 @@ const globalSetup = async () => {
.withWaitStrategy(Wait.forAll([Wait.forLogMessage('database system is ready to accept connections', 2)]))
.start();
const postgresPort = postgres.getMappedPort(5432);
const postgresPort = postgresContainer.getMappedPort(5432);
const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`;
process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl;
@ -55,6 +61,73 @@ const globalSetup = async () => {
await dataSource.initialize();
await dataSource.runMigrations();
await dataSource.destroy();
// for whatever reason, importing from test/utils causes vitest to crash
// eslint-disable-next-line unicorn/prefer-module
const migrationFolder = join(__dirname, '..', 'schema/migrations');
// TODO remove after we have at least one kysely migration
await mkdir(migrationFolder, { recursive: true });
const parsed = parse(process.env.IMMICH_TEST_POSTGRES_URL!);
const parsedOptions = {
...parsed,
ssl: false,
host: parsed.host ?? undefined,
port: parsed.port ? Number(parsed.port) : undefined,
database: parsed.database ?? undefined,
};
const driverOptions = {
...parsedOptions,
onnotice: (notice: Notice) => {
if (notice['severity'] !== 'NOTICE') {
console.warn('Postgres notice:', notice);
}
},
max: 10,
types: {
date: {
to: 1184,
from: [1082, 1114, 1184],
serialize: (x: Date | string) => (x instanceof Date ? x.toISOString() : x),
parse: (x: string) => new Date(x),
},
bigint: {
to: 20,
from: [20],
parse: (value: string) => Number.parseInt(value),
serialize: (value: number) => value.toString(),
},
},
connection: {
TimeZone: 'UTC',
},
};
const db = new Kysely({
dialect: new PostgresJSDialect({ postgres: postgres({ ...driverOptions, max: 1, database: 'postgres' }) }),
});
// TODO just call `databaseRepository.migrate()` (probably have to wait until TypeOrm is gone)
const migrator = new Migrator({
db,
migrationLockTableName: 'kysely_migrations_lock',
migrationTableName: 'kysely_migrations',
provider: new FileMigrationProvider({
fs: { readdir },
path: { join },
migrationFolder,
}),
});
const { error } = await migrator.migrateToLatest();
if (error) {
console.error('Unable to run kysely migrations', error);
throw error;
}
await db.destroy();
};
export default globalSetup;