refactor(server): domain/infra (#1298)

* refactor: user repository

* refactor: user module

* refactor: move database into infra

* refactor(cli): use user core

* chore: import path

* chore: tests
This commit is contained in:
Jason Rasmussen
2023-01-11 21:34:36 -05:00
committed by GitHub
parent 89a6ed2a5b
commit 131caa20eb
182 changed files with 701 additions and 676 deletions

View File

@@ -0,0 +1,17 @@
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
import { DataSource } from 'typeorm';
export const databaseConfig: PostgresConnectionOptions = {
type: 'postgres',
host: process.env.DB_HOSTNAME || 'immich_postgres',
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE_NAME,
entities: [__dirname + '/../**/*.entity.{js,ts}'],
synchronize: false,
migrations: [__dirname + '/../migrations/*.{js,ts}'],
migrationsRun: true,
};
export const dataSource = new DataSource(databaseConfig);

View File

@@ -0,0 +1 @@
export * from './database.config';

View File

@@ -0,0 +1,31 @@
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { AssetAlbumEntity } from './asset-album.entity';
import { SharedLinkEntity } from './shared-link.entity';
import { UserAlbumEntity } from './user-album.entity';
@Entity('albums')
export class AlbumEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
ownerId!: string;
@Column({ default: 'Untitled Album' })
albumName!: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: string;
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
albumThumbnailAssetId!: string | null;
@OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
sharedUsers?: UserAlbumEntity[];
@OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo)
assets?: AssetAlbumEntity[];
@OneToMany(() => SharedLinkEntity, (link) => link.album)
sharedLinks!: SharedLinkEntity[];
}

View File

@@ -0,0 +1,26 @@
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { UserEntity } from './user.entity';
@Entity('api_keys')
export class APIKeyEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column({ select: false })
key?: string;
@Column()
userId!: string;
@ManyToOne(() => UserEntity)
user?: UserEntity;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
}

View File

@@ -0,0 +1,31 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { AlbumEntity } from './album.entity';
import { AssetEntity } from './asset.entity';
@Entity('asset_album')
@Unique('UQ_unique_asset_in_album', ['albumId', 'assetId'])
export class AssetAlbumEntity {
@PrimaryGeneratedColumn()
id!: string;
@Column()
albumId!: string;
@Column()
@OneToOne(() => AssetEntity, (entity) => entity.id)
assetId!: string;
@ManyToOne(() => AlbumEntity, (album) => album.assets, {
onDelete: 'CASCADE',
nullable: true,
})
@JoinColumn({ name: 'albumId' })
albumInfo!: AlbumEntity;
@ManyToOne(() => AssetEntity, {
onDelete: 'CASCADE',
nullable: true,
})
@JoinColumn({ name: 'assetId' })
assetInfo!: AssetEntity;
}

View File

@@ -0,0 +1,83 @@
import { Column, Entity, Index, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { ExifEntity } from './exif.entity';
import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity';
import { TagEntity } from './tag.entity';
@Entity('assets')
@Unique('UQ_userid_checksum', ['userId', 'checksum'])
export class AssetEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
deviceAssetId!: string;
@Column()
userId!: string;
@Column()
deviceId!: string;
@Column()
type!: AssetType;
@Column()
originalPath!: string;
@Column({ type: 'varchar', nullable: true })
resizePath!: string | null;
@Column({ type: 'varchar', nullable: true, default: '' })
webpPath!: string | null;
@Column({ type: 'varchar', nullable: true, default: '' })
encodedVideoPath!: string;
@Column({ type: 'timestamptz' })
createdAt!: string;
@Column({ type: 'timestamptz' })
modifiedAt!: string;
@Column({ type: 'boolean', default: false })
isFavorite!: boolean;
@Column({ type: 'varchar', nullable: true })
mimeType!: string | null;
@Column({ type: 'bytea', nullable: true, select: false })
@Index({ where: `'checksum' IS NOT NULL` }) // avoid null index
checksum?: Buffer | null; // sha1 checksum
@Column({ type: 'varchar', nullable: true })
duration!: string | null;
@Column({ type: 'boolean', default: true })
isVisible!: boolean;
@Column({ type: 'uuid', nullable: true })
livePhotoVideoId!: string | null;
@OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
exifInfo?: ExifEntity;
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
smartInfo?: SmartInfoEntity;
// https://github.com/typeorm/typeorm/blob/master/docs/many-to-many-relations.md
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
@JoinTable({ name: 'tag_asset' })
tags!: TagEntity[];
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
@JoinTable({ name: 'shared_link__asset' })
sharedLinks!: SharedLinkEntity[];
}
export enum AssetType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
AUDIO = 'AUDIO',
OTHER = 'OTHER',
}

View File

@@ -0,0 +1,32 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity('device_info')
@Unique(['userId', 'deviceId'])
export class DeviceInfoEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column()
userId!: string;
@Column()
deviceId!: string;
@Column()
deviceType!: DeviceType;
@Column({ type: 'varchar', nullable: true })
notificationToken!: string | null;
@CreateDateColumn()
createdAt!: string;
@Column({ type: 'bool', default: false })
isAutoBackup!: boolean;
}
export enum DeviceType {
IOS = 'IOS',
ANDROID = 'ANDROID',
WEB = 'WEB',
}

View File

@@ -0,0 +1,101 @@
import { Index, JoinColumn, OneToOne } from 'typeorm';
import { Column } from 'typeorm/decorator/columns/Column';
import { PrimaryGeneratedColumn } from 'typeorm/decorator/columns/PrimaryGeneratedColumn';
import { Entity } from 'typeorm/decorator/entity/Entity';
import { AssetEntity } from './asset.entity';
@Entity('exif')
export class ExifEntity {
@PrimaryGeneratedColumn()
id!: string;
@Index({ unique: true })
@Column({ type: 'uuid' })
assetId!: string;
/* General info */
@Column({ type: 'text', nullable: true, default: '' })
description!: string; // or caption
@Column({ type: 'integer', nullable: true })
exifImageWidth!: number | null;
@Column({ type: 'integer', nullable: true })
exifImageHeight!: number | null;
@Column({ type: 'bigint', nullable: true })
fileSizeInByte!: number | null;
@Column({ type: 'varchar', nullable: true })
orientation!: string | null;
@Column({ type: 'timestamptz', nullable: true })
dateTimeOriginal!: Date | null;
@Column({ type: 'timestamptz', nullable: true })
modifyDate!: Date | null;
@Column({ type: 'float', nullable: true })
latitude!: number | null;
@Column({ type: 'float', nullable: true })
longitude!: number | null;
@Column({ type: 'varchar', nullable: true })
city!: string | null;
@Column({ type: 'varchar', nullable: true })
state!: string | null;
@Column({ type: 'varchar', nullable: true })
country!: string | null;
/* Image info */
@Column({ type: 'varchar', nullable: true })
make!: string | null;
@Column({ type: 'varchar', nullable: true })
model!: string | null;
@Column({ type: 'varchar', nullable: true })
imageName!: string | null;
@Column({ type: 'varchar', nullable: true })
lensModel!: string | null;
@Column({ type: 'float8', nullable: true })
fNumber!: number | null;
@Column({ type: 'float8', nullable: true })
focalLength!: number | null;
@Column({ type: 'integer', nullable: true })
iso!: number | null;
@Column({ type: 'float', nullable: true })
exposureTime!: number | null;
/* Video info */
@Column({ type: 'float8', nullable: true })
fps?: number | null;
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset?: AssetEntity;
@Index('exif_text_searchable', { synchronize: false })
@Column({
type: 'tsvector',
generatedType: 'STORED',
asExpression: `TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("imageName", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))`,
})
exifTextSearchableColumn!: string;
}

View File

@@ -0,0 +1,12 @@
export * from './album.entity';
export * from './api-key.entity';
export * from './asset-album.entity';
export * from './asset.entity';
export * from './device-info.entity';
export * from './exif.entity';
export * from './smart-info.entity';
export * from './system-config.entity';
export * from './tag.entity';
export * from './user-album.entity';
export * from './user.entity';
export * from './shared-link.entity';

View File

@@ -0,0 +1,50 @@
import { Column, Entity, Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { AlbumEntity } from './album.entity';
import { AssetEntity } from './asset.entity';
@Entity('shared_links')
@Unique('UQ_sharedlink_key', ['key'])
export class SharedLinkEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ nullable: true })
description?: string;
@Column()
userId!: string;
@Index('IDX_sharedlink_key')
@Column({ type: 'bytea' })
key!: Buffer; // use to access the inidividual asset
@Column()
type!: SharedLinkType;
@Column({ type: 'timestamptz' })
createdAt!: string;
@Column({ type: 'timestamptz', nullable: true })
expiresAt!: string | null;
@Column({ type: 'boolean', default: false })
allowUpload!: boolean;
@ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks)
assets!: AssetEntity[];
@ManyToOne(() => AlbumEntity, (album) => album.sharedLinks)
album?: AlbumEntity;
}
export enum SharedLinkType {
ALBUM = 'ALBUM',
/**
* Individual asset
* or group of assets that are not in an album
*/
INDIVIDUAL = 'INDIVIDUAL',
}
// npm run typeorm -- migration:generate ./libs/database/src/AddSharedLinkTable -d libs/database/src/config/database.config.ts

View File

@@ -0,0 +1,22 @@
import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AssetEntity } from './asset.entity';
@Entity('smart_info')
export class SmartInfoEntity {
@PrimaryGeneratedColumn()
id!: string;
@Index({ unique: true })
@Column({ type: 'uuid' })
assetId!: string;
@Column({ type: 'text', array: true, nullable: true })
tags!: string[] | null;
@Column({ type: 'text', array: true, nullable: true })
objects!: string[] | null;
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset?: AssetEntity;
}

View File

@@ -0,0 +1,61 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('system_config')
export class SystemConfigEntity<T = string | boolean> {
@PrimaryColumn()
key!: SystemConfigKey;
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
value!: T;
}
export type SystemConfigValue = any;
// dot notation matches path in `SystemConfig`
export enum SystemConfigKey {
FFMPEG_CRF = 'ffmpeg.crf',
FFMPEG_PRESET = 'ffmpeg.preset',
FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec',
FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec',
FFMPEG_TARGET_SCALING = 'ffmpeg.targetScaling',
OAUTH_ENABLED = 'oauth.enabled',
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
OAUTH_CLIENT_ID = 'oauth.clientId',
OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
OAUTH_SCOPE = 'oauth.scope',
OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch',
OAUTH_BUTTON_TEXT = 'oauth.buttonText',
OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
OAUTH_MOBILE_OVERRIDE_ENABLED = 'oauth.mobileOverrideEnabled',
OAUTH_MOBILE_REDIRECT_URI = 'oauth.mobileRedirectUri',
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
STORAGE_TEMPLATE = 'storageTemplate.template',
}
export interface SystemConfig {
ffmpeg: {
crf: string;
preset: string;
targetVideoCodec: string;
targetAudioCodec: string;
targetScaling: string;
};
oauth: {
enabled: boolean;
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
buttonText: string;
autoRegister: boolean;
autoLaunch: boolean;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
};
passwordLogin: {
enabled: boolean;
};
storageTemplate: {
template: string;
};
}

View File

@@ -0,0 +1,45 @@
import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { AssetEntity } from './asset.entity';
import { UserEntity } from './user.entity';
@Entity('tags')
@Unique('UQ_tag_name_userId', ['name', 'userId'])
export class TagEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
type!: TagType;
@Column()
name!: string;
@Column()
userId!: string;
@Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true })
renameTagId!: string;
@ManyToMany(() => AssetEntity, (asset) => asset.tags)
assets!: AssetEntity[];
@ManyToOne(() => UserEntity, (user) => user.tags)
user!: UserEntity;
}
export enum TagType {
/**
* Tag that is detected by the ML model for object detection will use this type
*/
OBJECT = 'OBJECT',
/**
* Face that is detected by the ML model for facial detection (TBD/NOT YET IMPLEMENTED) will use this type
*/
FACE = 'FACE',
/**
* Tag that is created by the user will use this type
*/
CUSTOM = 'CUSTOM',
}

View File

@@ -0,0 +1,27 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { UserEntity } from './user.entity';
import { AlbumEntity } from './album.entity';
@Entity('user_shared_album')
@Unique('PK_unique_user_in_album', ['albumId', 'sharedUserId'])
export class UserAlbumEntity {
@PrimaryGeneratedColumn()
id!: string;
@Column()
albumId!: string;
@Column()
sharedUserId!: string;
@ManyToOne(() => AlbumEntity, (album) => album.sharedUsers, {
onDelete: 'CASCADE',
nullable: true,
})
@JoinColumn({ name: 'albumId' })
albumInfo!: AlbumEntity;
@ManyToOne(() => UserEntity)
@JoinColumn({ name: 'sharedUserId' })
userInfo!: UserEntity;
}

View File

@@ -0,0 +1,41 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { TagEntity } from './tag.entity';
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ default: '' })
firstName!: string;
@Column({ default: '' })
lastName!: string;
@Column({ default: false })
isAdmin!: boolean;
@Column({ unique: true })
email!: string;
@Column({ default: '', select: false })
password?: string;
@Column({ default: '' })
oauthId!: string;
@Column({ default: '' })
profileImagePath!: string;
@Column({ default: true })
shouldChangePassword!: boolean;
@CreateDateColumn()
createdAt!: string;
@DeleteDateColumn()
deletedAt?: Date;
@OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[];
}

View File

@@ -0,0 +1,3 @@
export * from './config';
export * from './entities';
export * from './repository';

View File

@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserTable1645130759468 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists users
(
id uuid default uuid_generate_v4() not null
constraint "PK_a3ffb1c0c8416b9fc6f907b7433"
primary key,
email varchar not null,
password varchar not null,
salt varchar not null,
"createdAt" timestamp default now() not null
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table users`);
}
}

View File

@@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateDeviceInfoTable1645130777674 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists device_info
(
id serial
constraint "PK_b1c15a80b0a4e5f4eebadbdd92c"
primary key,
"userId" varchar not null,
"deviceId" varchar not null,
"deviceType" varchar not null,
"notificationToken" varchar,
"createdAt" timestamp default now() not null,
"isAutoBackup" boolean default false not null,
constraint "UQ_ebad78f36b10d15fbea8560e107"
unique ("userId", "deviceId")
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table device_info`);
}
}

View File

@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateAssetsTable1645130805273 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists assets
(
id uuid default uuid_generate_v4() not null
constraint "PK_da96729a8b113377cfb6a62439c"
primary key,
"deviceAssetId" varchar not null,
"userId" varchar not null,
"deviceId" varchar not null,
type varchar not null,
"originalPath" varchar not null,
"resizePath" varchar,
"createdAt" varchar not null,
"modifiedAt" varchar not null,
"isFavorite" boolean default false not null,
"mimeType" varchar,
duration varchar,
constraint "UQ_b599ab0bd9574958acb0b30a90e"
unique ("deviceAssetId", "userId", "deviceId")
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table assets`);
}
}

View File

@@ -0,0 +1,42 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateExifTable1645130817965 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists exif
(
id serial
constraint "PK_28663352d85078ad0046dafafaa"
primary key,
"assetId" uuid not null
constraint "REL_c0117fdbc50b917ef9067740c4"
unique
constraint "FK_c0117fdbc50b917ef9067740c44"
references assets
on delete cascade,
make varchar,
model varchar,
"imageName" varchar,
"exifImageWidth" integer,
"exifImageHeight" integer,
"fileSizeInByte" integer,
orientation varchar,
"dateTimeOriginal" timestamp with time zone,
"modifyDate" timestamp with time zone,
"lensModel" varchar,
"fNumber" double precision,
"focalLength" double precision,
iso integer,
"exposureTime" double precision,
latitude double precision,
longitude double precision
);
create unique index if not exists "IDX_c0117fdbc50b917ef9067740c4" on exif ("assetId");
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table exif`);
}
}

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSmartInfoTable1645130870184 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists smart_info
(
id serial
constraint "PK_0beace66440e9713f5c40470e46"
primary key,
"assetId" uuid not null
constraint "UQ_5e3753aadd956110bf3ec0244ac"
unique
constraint "FK_5e3753aadd956110bf3ec0244ac"
references assets
on delete cascade,
tags text[]
);
create unique index if not exists "IDX_5e3753aadd956110bf3ec0244a"
on smart_info ("assetId");
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
drop table smart_info;
`);
}
}

View File

@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddExifTextSearchColumn1646249209023 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
GENERATED ALWAYS AS (
TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '')
)
) STORED;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN IF EXISTS exif_text_searchable_column;
`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateExifTextSearchIndex1646249734844 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE INDEX exif_text_searchable_idx
ON exif
USING GIN (exif_text_searchable_column);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX IF EXISTS exif_text_searchable_idx ON exif;
`);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddRegionCityToExIf1646709533213 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
ADD COLUMN if not exists city varchar;
ALTER TABLE exif
ADD COLUMN if not exists state varchar;
ALTER TABLE exif
ADD COLUMN if not exists country varchar;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN city;
ALTER TABLE exif
DROP COLUMN state;
ALTER TABLE exif
DROP COLUMN country;
`);
}
}

View File

@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddLocationToExifTextSearch1646710459852 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN IF EXISTS exif_text_searchable_column;
ALTER TABLE exif
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
GENERATED ALWAYS AS (
TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", '')
)
) STORED;
CREATE INDEX exif_text_searchable_idx
ON exif
USING GIN (exif_text_searchable_column);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN IF EXISTS exif_text_searchable_column;
DROP INDEX IF EXISTS exif_text_searchable_idx ON exif;
`);
}
}

View File

@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddObjectColumnToSmartInfo1648317474768 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE smart_info
ADD COLUMN if not exists objects text[];
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE smart_info
DROP COLUMN objects;
`);
}
}

View File

@@ -0,0 +1,70 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSharedAlbumAndRelatedTables1649643216111 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Create shared_albums
await queryRunner.query(`
create table if not exists shared_albums
(
id uuid default uuid_generate_v4() not null
constraint "PK_7f71c7b5bc7c87b8f94c9a93a00"
primary key,
"ownerId" varchar not null,
"albumName" varchar default 'Untitled Album'::character varying not null,
"createdAt" timestamp with time zone default now() not null,
"albumThumbnailAssetId" varchar
);
comment on column shared_albums."albumThumbnailAssetId" is 'Asset ID to be used as thumbnail';
`);
// Create user_shared_album
await queryRunner.query(`
create table if not exists user_shared_album
(
id serial
constraint "PK_b6562316a98845a7b3e9a25cdd0"
primary key,
"albumId" uuid not null
constraint "FK_7b3bf0f5f8da59af30519c25f18"
references shared_albums
on delete cascade,
"sharedUserId" uuid not null
constraint "FK_543c31211653e63e080ba882eb5"
references users,
constraint "PK_unique_user_in_album"
unique ("albumId", "sharedUserId")
);
`);
// Create asset_shared_album
await queryRunner.query(
`
create table if not exists asset_shared_album
(
id serial
constraint "PK_a34e076afbc601d81938e2c2277"
primary key,
"albumId" uuid not null
constraint "FK_a8b79a84996cef6ba6a3662825d"
references shared_albums
on delete cascade,
"assetId" uuid not null
constraint "FK_64f2e7d68d1d1d8417acc844a4a"
references assets
on delete cascade,
constraint "UQ_a1e2734a1ce361e7a26f6b28288"
unique ("albumId", "assetId")
);
`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
drop table asset_shared_album;
drop table user_shared_album;
drop table shared_albums;
`);
}
}

View File

@@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table users
add column if not exists "firstName" varchar default '';
alter table users
add column if not exists "lastName" varchar default '';
alter table users
add column if not exists "profileImagePath" varchar default '';
alter table users
add column if not exists "isAdmin" bool default false;
alter table users
add column if not exists "isFirstLoggedIn" bool default true;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table users
drop column "firstName";
alter table users
drop column "lastName";
alter table users
drop column "isAdmin";
`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateAssetTableWithWebpPath1653214255670 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table assets
add column if not exists "webpPath" varchar default '';
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table assets
drop column if exists "webpPath";
`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateAssetTableWithEncodeVideoPath1654299904583 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table assets
add column if not exists "encodedVideoPath" varchar default '';
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table assets
drop column if exists "encodedVideoPath";
`);
}
}

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameSharedAlbums1655401127251 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE shared_albums RENAME TO albums;
ALTER TABLE asset_shared_album RENAME TO asset_album;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE asset_album RENAME TO asset_shared_album;
ALTER TABLE albums RENAME TO shared_albums;
`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameIsFirstLoggedInColumn1656338626260 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE users
RENAME COLUMN "isFirstLoggedIn" to "shouldChangePassword";
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE users
RENAME COLUMN "shouldChangePassword" to "isFirstLoggedIn";
`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameAssetAlbumIdSequence1656888591977 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
await queryRunner.query(
`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
await queryRunner.query(
`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`,
);
}
}

View File

@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class DropExifTextSearchableColumns1656888918620 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN IF EXISTS exif_text_searchable_column;
ALTER TABLE exif
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
GENERATED ALWAYS AS (
TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", '')
)
) STORED;
CREATE INDEX exif_text_searchable_idx
ON exif
USING GIN (exif_text_searchable_column);
`);
}
}

View File

@@ -0,0 +1,72 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MatchMigrationsWithTypeORMEntities1656889061566 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))) STORED`);
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'postgres', 'public', 'exif'],
);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
[
'postgres',
'public',
'exif',
'GENERATED_COLUMN',
'exifTextSearchableColumn',
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
],
);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
await queryRunner.query(
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`,
);
await queryRunner.query(
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
await queryRunner.query(
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`,
);
await queryRunner.query(
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}

View File

@@ -0,0 +1,50 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddExifImageNameAsSearchableText1658860470248 implements MigrationInterface {
name = 'AddExifImageNameAsSearchableText1658860470248';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("imageName", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", ''))) STORED`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
[
'immich',
'public',
'exif',
'GENERATED_COLUMN',
'exifTextSearchableColumn',
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"imageName\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
],
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
['immich', 'public', 'exif', 'GENERATED_COLUMN', 'exifTextSearchableColumn', ''],
);
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector NOT NULL`);
}
}

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCaption1661011331242 implements MigrationInterface {
name = 'AddCaption1661011331242';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" ADD "description" text DEFAULT ''`);
await queryRunner.query(`ALTER TABLE "exif" ADD "fps" double precision`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "fps"`);
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "description"`);
}
}

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class ChangeExifFileSizeInByteToBigInt1661528919411 implements MigrationInterface {
name = 'ChangeExifFileSizeInByteToBigInt1661528919411';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
ALTER COLUMN "fileSizeInByte" type bigint using "fileSizeInByte"::bigint;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
ALTER COLUMN "fileSizeInByte" type integer using "fileSizeInByte"::integer;
`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAssetChecksum1661881837496 implements MigrationInterface {
name = 'AddAssetChecksum1661881837496';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "checksum" bytea`);
await queryRunner.query(
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements MigrationInterface {
name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e"`);
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_userid_checksum" UNIQUE ("userId", "checksum")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_userid_checksum"`);
await queryRunner.query(
`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`,
);
}
}

View File

@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class FixTimestampDataTypeInAssetTable1662427365521 implements MigrationInterface {
name = 'FixTimestampDataTypeInAssetTable1662427365521';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
await queryRunner.query(
`ALTER TABLE "assets" ALTER COLUMN "createdAt" TYPE timestamptz USING "createdAt"::timestamptz`,
);
await queryRunner.query(
`ALTER TABLE "assets" ALTER COLUMN "modifiedAt" TYPE timestamptz USING "createdAt"::timestamptz`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "createdAt" TYPE varchar USING "createdAt"::varchar`);
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "modifiedAt" TYPE varchar USING "createdAt"::varchar`);
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" DROP NOT NULL`);
}
}

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSystemConfigTable1665540663419 implements MigrationInterface {
name = 'CreateSystemConfigTable1665540663419';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" character varying, CONSTRAINT "PK_aab69295b445016f56731f4d535" PRIMARY KEY ("key"))`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "system_config"`);
}
}

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddingDeletedAtColumnInUserEntity1667762360744 implements MigrationInterface {
name = 'AddingDeletedAtColumnInUserEntity1667762360744';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "deletedAt" TIMESTAMP`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "deletedAt"`);
}
}

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddLivePhotosRelatedColumnToAssetTable1668383120461 implements MigrationInterface {
name = 'AddLivePhotosRelatedColumnToAssetTable1668383120461'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "isVisible" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "assets" ADD "livePhotoVideoId" uuid`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "livePhotoVideoId"`);
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isVisible"`);
}
}

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdateUserTableForOIDC1668835311083 implements MigrationInterface {
name = 'UpdateUserTableForOIDC1668835311083'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "password" SET DEFAULT ''`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "salt" SET DEFAULT ''`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "salt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "password" DROP DEFAULT`);
}
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class OAuthId1670104716264 implements MigrationInterface {
name = 'OAuthId1670104716264'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "oauthId" character varying NOT NULL DEFAULT ''`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "oauthId"`);
}
}

View File

@@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateTagsTable1670257571385 implements MigrationInterface {
name = 'CreateTagsTable1670257571385'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL, "name" character varying NOT NULL, "userId" uuid NOT NULL, "renameTagId" uuid, CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId"), CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id")); COMMENT ON COLUMN "tags"."renameTagId" IS 'The new renamed tagId'`);
await queryRunner.query(`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL, CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId"))`);
await queryRunner.query(`CREATE INDEX "IDX_f8e8a9e893cb5c54907f1b798e" ON "tag_asset" ("assetsId") `);
await queryRunner.query(`CREATE INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" ON "tag_asset" ("tagsId") `);
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`);
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`);
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`);
await queryRunner.query(`DROP INDEX "public"."IDX_e99f31ea4cdf3a2c35c7287eb4"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f8e8a9e893cb5c54907f1b798e"`);
await queryRunner.query(`DROP TABLE "tag_asset"`);
await queryRunner.query(`DROP TABLE "tags"`);
}
}

View File

@@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class TruncateOldConfigItems1670607437008 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`TRUNCATE TABLE "system_config"`);
}
public async down(): Promise<void> {
// noop
}
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserEmailUniqueConstraint1670633210032 implements MigrationInterface {
name = 'AddUserEmailUniqueConstraint1670633210032'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3"`);
}
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class DropSaltColumn1672109862870 implements MigrationInterface {
name = 'DropSaltColumn1672109862870'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "salt"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "salt" character varying NOT NULL DEFAULT ''`);
}
}

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAPIKeys1672502270115 implements MigrationInterface {
name = 'AddAPIKeys1672502270115'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "api_keys" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`);
await queryRunner.query(`DROP TABLE "api_keys"`);
}
}

View File

@@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddSharedLinkTable1673150490490 implements MigrationInterface {
name = 'AddSharedLinkTable1673150490490'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" character varying NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key"), CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_sharedlink_key" ON "shared_links" ("key") `);
await queryRunner.query(`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL, CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId"))`);
await queryRunner.query(`CREATE INDEX "IDX_5b7decce6c8d3db9593d6111a6" ON "shared_link__asset" ("assetsId") `);
await queryRunner.query(`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId") `);
await queryRunner.query(`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`);
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`);
await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`);
await queryRunner.query(`DROP INDEX "public"."IDX_c9fab4aa97ffd1b034f3d6581a"`);
await queryRunner.query(`DROP INDEX "public"."IDX_5b7decce6c8d3db9593d6111a6"`);
await queryRunner.query(`DROP TABLE "shared_link__asset"`);
await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_key"`);
await queryRunner.query(`DROP TABLE "shared_links"`);
}
}

View File

@@ -0,0 +1 @@
export * from './user.repository';

View File

@@ -0,0 +1,71 @@
import { UserEntity } from '../entities';
import { IUserRepository, UserListFilter } from '@app/domain';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
@Injectable()
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
async get(userId: string, withDeleted?: boolean): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { id: userId }, withDeleted: withDeleted });
}
async getAdmin(): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { isAdmin: true } });
}
async getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | null> {
let builder = this.userRepository.createQueryBuilder('user').where({ email });
if (withPassword) {
builder = builder.addSelect('user.password');
}
return builder.getOne();
}
async getByOAuthId(oauthId: string): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { oauthId } });
}
async getList({ excludeId }: UserListFilter = {}): Promise<UserEntity[]> {
if (!excludeId) {
return this.userRepository.find(); // TODO: this should also be ordered the same as below
}
return this.userRepository.find({
where: { id: Not(excludeId) },
withDeleted: true,
order: {
createdAt: 'DESC',
},
});
}
async create(user: Partial<UserEntity>): Promise<UserEntity> {
return this.userRepository.save(user);
}
async update(id: string, user: Partial<UserEntity>): Promise<UserEntity> {
user.id = id;
await this.userRepository.save(user);
const updatedUser = await this.get(id);
if (!updatedUser) {
throw new InternalServerErrorException('Cannot reload user after update');
}
return updatedUser;
}
async delete(user: UserEntity): Promise<UserEntity> {
return this.userRepository.softRemove(user);
}
async restore(user: UserEntity): Promise<UserEntity> {
return this.userRepository.recover(user);
}
}

View File

@@ -0,0 +1,2 @@
export * from './db';
export * from './infra.module';

View File

@@ -0,0 +1,22 @@
import { databaseConfig, UserEntity } from '@app/infra';
import { IUserRepository } from '@app/domain';
import { Global, Module, Provider } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserRepository } from './db';
const providers: Provider[] = [
//
{ provide: IUserRepository, useClass: UserRepository },
];
@Global()
@Module({
imports: [
//
TypeOrmModule.forRoot(databaseConfig),
TypeOrmModule.forFeature([UserEntity]),
],
providers: [...providers],
exports: [...providers],
})
export class InfraModule {}