mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
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:
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
Reference in New Issue
Block a user