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

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

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

View File

@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock'; 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 { InjectKysely } from 'nestjs-kysely';
import { mkdir, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import semver from 'semver'; import semver from 'semver';
import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
import { DB } from 'src/db'; import { DB } from 'src/db';
@ -200,9 +202,49 @@ export class DatabaseRepository {
this.logger.log('Running migrations, this may take a while'); this.logger.log('Running migrations, this may take a while');
this.logger.debug('Running typeorm migrations');
await dataSource.initialize(); await dataSource.initialize();
await dataSource.runMigrations(options); await dataSource.runMigrations(options);
await dataSource.destroy(); 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> { 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 { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserEntity, withMetadata } from 'src/entities/user.entity'; import { UserEntity, withMetadata } from 'src/entities/user.entity';
import { AssetType, UserStatus } from 'src/enum'; 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'; import { asUuid } from 'src/utils/database';
type Upsert = Insertable<DbUserMetadata>; 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 { import {
Check, Check,
Column, Column,
@ -10,9 +13,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } 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') @Table('activity')
@Index({ @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 { 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' }) @Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
export class AlbumAssetTable { export class AlbumAssetTable {

View File

@ -1,7 +1,7 @@
import { AlbumUserRole } from 'src/enum'; 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 { 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' }) @Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' })
// Pre-existing indices from original album <--> user ManyToMany mapping // Pre-existing indices from original album <--> user ManyToMany mapping

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { SourceType } from 'src/enum'; 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 { 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' }) @Table({ name: 'asset_faces' })
@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] }) @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 { Column, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
@Table('asset_job_status') @Table('asset_job_status')
export class AssetJobStatusTable { export class AssetJobStatusTable {

View File

@ -1,5 +1,8 @@
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum'; 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 { import {
Column, Column,
ColumnIndex, ColumnIndex,
@ -12,9 +15,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } 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') @Table('assets')
// Checksums must be unique per user and library // 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 { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn, UpdateIdColumn } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
@Table('exif') @Table('exif')
export class ExifTable { 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 { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetFaceTable } from 'src/tables/asset-face.table';
@Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' }) @Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' })
export class FaceSearchTable { 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 { import {
Column, Column,
ColumnIndex, ColumnIndex,
@ -9,7 +10,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('libraries') @Table('libraries')
export class LibraryTable { export class LibraryTable {

View File

@ -1,4 +1,5 @@
import { MemoryType } from 'src/enum'; import { MemoryType } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import { import {
Column, Column,
ColumnIndex, ColumnIndex,
@ -10,7 +11,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
import { MemoryData } from 'src/types'; import { MemoryData } from 'src/types';
@Table('memories') @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 { 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') @Table('memories_assets_assets')
export class MemoryAssetTable { export class MemoryAssetTable {

View File

@ -1,3 +1,4 @@
import { UserTable } from 'src/schema/tables/user.table';
import { import {
Column, Column,
ColumnIndex, ColumnIndex,
@ -7,7 +8,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('partners') @Table('partners')
export class PartnerTable { 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 { import {
Check, Check,
Column, Column,
@ -9,8 +11,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } from 'src/sql-tools';
import { AssetFaceTable } from 'src/tables/asset-face.table';
import { UserTable } from 'src/tables/user.table';
@Table('person') @Table('person')
@Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` }) @Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` })

View File

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

View File

@ -1,4 +1,6 @@
import { SharedLinkType } from 'src/enum'; import { SharedLinkType } from 'src/enum';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
import { import {
Column, Column,
ColumnIndex, ColumnIndex,
@ -8,8 +10,6 @@ import {
Table, Table,
Unique, Unique,
} from 'src/sql-tools'; } from 'src/sql-tools';
import { AlbumTable } from 'src/tables/album.table';
import { UserTable } from 'src/tables/user.table';
@Table('shared_links') @Table('shared_links')
@Unique({ name: 'UQ_sharedlink_key', columns: ['key'] }) @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 { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
@Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' }) @Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' })
export class SmartSearchTable { 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 { ForeignKeyColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
import { AssetTable } from 'src/tables/asset.table';
import { UserTable } from 'src/tables/user.table';
@Table('asset_stack') @Table('asset_stack')
export class StackTable { export class StackTable {

View File

@ -1,4 +1,5 @@
import { SyncEntityType } from 'src/enum'; import { SyncEntityType } from 'src/enum';
import { SessionTable } from 'src/schema/tables/session.table';
import { import {
Column, Column,
ColumnIndex, ColumnIndex,
@ -9,7 +10,6 @@ import {
UpdateDateColumn, UpdateDateColumn,
UpdateIdColumn, UpdateIdColumn,
} from 'src/sql-tools'; } from 'src/sql-tools';
import { SessionTable } from 'src/tables/session.table';
@Table('session_sync_checkpoints') @Table('session_sync_checkpoints')
export class SessionSyncCheckpointTable { 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 { 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'] }) @Index({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] })
@Table('tag_asset') @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 { ColumnIndex, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools';
import { TagTable } from 'src/tables/tag.table';
@Table('tags_closure') @Table('tags_closure')
export class TagClosureTable { export class TagClosureTable {

View File

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

View File

@ -1,7 +1,7 @@
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity'; import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserMetadataKey } from 'src/enum'; import { UserMetadataKey } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools';
import { UserTable } from 'src/tables/user.table';
@Table('user_metadata') @Table('user_metadata')
export class UserMetadataTable<T extends keyof UserMetadata = UserMetadataKey> implements UserMetadataItem<T> { 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 { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-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 { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
import { getConfig, updateConfig } from 'src/utils/config'; 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 { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-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 { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { newUuid } from 'test/small.factory'; import { newUuid } from 'test/small.factory';
import { automock } from 'test/utils'; 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 { GenericContainer, Wait } from 'testcontainers';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
const globalSetup = async () => { 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) .withExposedPorts(5432)
.withEnvironment({ .withEnvironment({
POSTGRES_PASSWORD: 'postgres', POSTGRES_PASSWORD: 'postgres',
@ -29,7 +35,7 @@ const globalSetup = async () => {
.withWaitStrategy(Wait.forAll([Wait.forLogMessage('database system is ready to accept connections', 2)])) .withWaitStrategy(Wait.forAll([Wait.forLogMessage('database system is ready to accept connections', 2)]))
.start(); .start();
const postgresPort = postgres.getMappedPort(5432); const postgresPort = postgresContainer.getMappedPort(5432);
const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`; const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`;
process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl; process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl;
@ -55,6 +61,73 @@ const globalSetup = async () => {
await dataSource.initialize(); await dataSource.initialize();
await dataSource.runMigrations(); await dataSource.runMigrations();
await dataSource.destroy(); 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; export default globalSetup;