mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 05:34:32 -04:00
feat: kysely migrations (#17198)
This commit is contained in:
parent
6fa0cb534a
commit
55a3c30664
@ -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
2
server/src/db.d.ts
vendored
@ -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>;
|
||||||
|
@ -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> {
|
||||||
|
@ -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>;
|
||||||
|
@ -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({
|
@ -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 {
|
@ -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
|
@ -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 {
|
@ -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 {
|
@ -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'] })
|
@ -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 {
|
@ -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
|
@ -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 {
|
@ -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 {
|
73
server/src/schema/tables/index.ts
Normal file
73
server/src/schema/tables/index.ts
Normal 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,
|
||||||
|
];
|
@ -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 {
|
@ -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')
|
@ -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 {
|
@ -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 {
|
@ -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` })
|
@ -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 {
|
@ -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 {
|
@ -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'] })
|
@ -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 {
|
@ -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 {
|
@ -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 {
|
@ -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')
|
@ -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 {
|
@ -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'] })
|
@ -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> {
|
@ -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';
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
];
|
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user