mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
chore: migrate database files (#8126)
This commit is contained in:
parent
84f7ca855a
commit
c1402eee8e
310 changed files with 358 additions and 362 deletions
51
server/src/entities/activity.entity.ts
Normal file
51
server/src/entities/activity.entity.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Check,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('activity')
|
||||
@Index('IDX_activity_like', ['assetId', 'userId', 'albumId'], { unique: true, where: '("isLiked" = true)' })
|
||||
@Check(`("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false)`)
|
||||
export class ActivityEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column()
|
||||
albumId!: string;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
assetId!: string | null;
|
||||
|
||||
@Column({ type: 'text', default: null })
|
||||
comment!: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isLiked!: boolean;
|
||||
|
||||
@ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
asset!: AssetEntity | null;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
user!: UserEntity;
|
||||
|
||||
@ManyToOne(() => AlbumEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
album!: AlbumEntity;
|
||||
}
|
||||
71
server/src/entities/album.entity.ts
Normal file
71
server/src/entities/album.entity.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
// ran into issues when importing the enum from `asset.dto.ts`
|
||||
export enum AssetOrder {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
@Entity('albums')
|
||||
export class AlbumEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@Column({ default: 'Untitled Album' })
|
||||
albumName!: string;
|
||||
|
||||
@Column({ type: 'text', default: '' })
|
||||
description!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt!: Date | null;
|
||||
|
||||
@ManyToOne(() => AssetEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||
albumThumbnailAsset!: AssetEntity | null;
|
||||
|
||||
@Column({ comment: 'Asset ID to be used as thumbnail', nullable: true })
|
||||
albumThumbnailAssetId!: string | null;
|
||||
|
||||
@ManyToMany(() => UserEntity)
|
||||
@JoinTable()
|
||||
sharedUsers!: UserEntity[];
|
||||
|
||||
@ManyToMany(() => AssetEntity, (asset) => asset.albums)
|
||||
@JoinTable()
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
|
||||
@Column({ default: true })
|
||||
isActivityEnabled!: boolean;
|
||||
|
||||
@Column({ type: 'varchar', default: AssetOrder.DESC })
|
||||
order!: AssetOrder;
|
||||
}
|
||||
26
server/src/entities/api-key.entity.ts
Normal file
26
server/src/entities/api-key.entity.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('api_keys')
|
||||
export class APIKeyEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
name!: string;
|
||||
|
||||
@Column({ select: false })
|
||||
key?: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||
user?: UserEntity;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
49
server/src/entities/asset-face.entity.ts
Normal file
49
server/src/entities/asset-face.entity.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('asset_faces', { synchronize: false })
|
||||
@Index('IDX_asset_faces_assetId_personId', ['assetId', 'personId'])
|
||||
@Index(['personId', 'assetId'])
|
||||
export class AssetFaceEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
assetId!: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
personId!: string | null;
|
||||
|
||||
@Index('face_index', { synchronize: false })
|
||||
@Column({ type: 'float4', array: true, select: false, transformer: { from: (v) => JSON.parse(v), to: (v) => v } })
|
||||
embedding!: number[];
|
||||
|
||||
@Column({ default: 0, type: 'int' })
|
||||
imageWidth!: number;
|
||||
|
||||
@Column({ default: 0, type: 'int' })
|
||||
imageHeight!: number;
|
||||
|
||||
@Column({ default: 0, type: 'int' })
|
||||
boundingBoxX1!: number;
|
||||
|
||||
@Column({ default: 0, type: 'int' })
|
||||
boundingBoxY1!: number;
|
||||
|
||||
@Column({ default: 0, type: 'int' })
|
||||
boundingBoxX2!: number;
|
||||
|
||||
@Column({ default: 0, type: 'int' })
|
||||
boundingBoxY2!: number;
|
||||
|
||||
@ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
asset!: AssetEntity;
|
||||
|
||||
@ManyToOne(() => PersonEntity, (person) => person.faces, {
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
person!: PersonEntity | null;
|
||||
}
|
||||
18
server/src/entities/asset-job-status.entity.ts
Normal file
18
server/src/entities/asset-job-status.entity.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('asset_job_status')
|
||||
export class AssetJobStatusEntity {
|
||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn()
|
||||
asset!: AssetEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
assetId!: string;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
facesRecognizedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
metadataExtractedAt!: Date | null;
|
||||
}
|
||||
19
server/src/entities/asset-stack.entity.ts
Normal file
19
server/src/entities/asset-stack.entity.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('asset_stack')
|
||||
export class AssetStackEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@OneToMany(() => AssetEntity, (asset) => asset.stack)
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@OneToOne(() => AssetEntity)
|
||||
@JoinColumn()
|
||||
//TODO: Add constraint to ensure primary asset exists in the assets array
|
||||
primaryAsset!: AssetEntity;
|
||||
|
||||
@Column({ nullable: false })
|
||||
primaryAssetId!: string;
|
||||
}
|
||||
178
server/src/entities/asset.entity.ts
Normal file
178
server/src/entities/asset.entity.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
|
||||
|
||||
@Entity('assets')
|
||||
// Checksums must be unique per user and library
|
||||
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'library', 'checksum'], {
|
||||
unique: true,
|
||||
})
|
||||
@Index('IDX_day_of_month', { synchronize: false })
|
||||
@Index('IDX_month', { synchronize: false })
|
||||
@Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId'])
|
||||
@Index('IDX_asset_id_stackId', ['id', 'stackId'])
|
||||
@Index('idx_originalFileName_trigram', { synchronize: false })
|
||||
// For all assets, each originalpath must be unique per user and library
|
||||
export class AssetEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
deviceAssetId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@ManyToOne(() => LibraryEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
library!: LibraryEntity;
|
||||
|
||||
@Column()
|
||||
libraryId!: 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: 'bytea', nullable: true })
|
||||
thumbhash!: Buffer | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, default: '' })
|
||||
encodedVideoPath!: string | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'timestamptz', nullable: true })
|
||||
deletedAt!: Date | null;
|
||||
|
||||
@Index('idx_asset_file_created_at')
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileCreatedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
localDateTime!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileModifiedAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isFavorite!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isArchived!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isExternal!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isReadOnly!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isOffline!: boolean;
|
||||
|
||||
@Column({ type: 'bytea' })
|
||||
@Index()
|
||||
checksum!: Buffer; // sha1 checksum
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
duration!: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: boolean;
|
||||
|
||||
@OneToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||
@JoinColumn()
|
||||
livePhotoVideo!: AssetEntity | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
livePhotoVideoId!: string | null;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
@Index()
|
||||
originalFileName!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
sidecarPath!: string | null;
|
||||
|
||||
@OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
|
||||
exifInfo?: ExifEntity;
|
||||
|
||||
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
|
||||
smartInfo?: SmartInfoEntity;
|
||||
|
||||
@OneToOne(() => SmartSearchEntity, (smartSearchEntity) => smartSearchEntity.asset)
|
||||
smartSearch?: SmartSearchEntity;
|
||||
|
||||
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
|
||||
@JoinTable({ name: 'tag_asset', synchronize: false })
|
||||
tags!: TagEntity[];
|
||||
|
||||
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
|
||||
@JoinTable({ name: 'shared_link__asset' })
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
|
||||
@ManyToMany(() => AlbumEntity, (album) => album.assets, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
albums?: AlbumEntity[];
|
||||
|
||||
@OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.asset)
|
||||
faces!: AssetFaceEntity[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
stackId?: string | null;
|
||||
|
||||
@ManyToOne(() => AssetStackEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||
@JoinColumn()
|
||||
stack?: AssetStackEntity | null;
|
||||
|
||||
@OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true })
|
||||
jobStatus?: AssetJobStatusEntity;
|
||||
}
|
||||
|
||||
export enum AssetType {
|
||||
IMAGE = 'IMAGE',
|
||||
VIDEO = 'VIDEO',
|
||||
AUDIO = 'AUDIO',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
34
server/src/entities/audit.entity.ts
Normal file
34
server/src/entities/audit.entity.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
export enum DatabaseAction {
|
||||
CREATE = 'CREATE',
|
||||
UPDATE = 'UPDATE',
|
||||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
export enum EntityType {
|
||||
ASSET = 'ASSET',
|
||||
ALBUM = 'ALBUM',
|
||||
}
|
||||
|
||||
@Entity('audit')
|
||||
@Index('IDX_ownerId_createdAt', ['ownerId', 'createdAt'])
|
||||
export class AuditEntity {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
entityType!: EntityType;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
entityId!: string;
|
||||
|
||||
@Column()
|
||||
action!: DatabaseAction;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
ownerId!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
117
server/src/entities/exif.entity.ts
Normal file
117
server/src/entities/exif.entity.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
import { Column } from 'typeorm/decorator/columns/Column.js';
|
||||
import { Entity } from 'typeorm/decorator/entity/Entity.js';
|
||||
|
||||
@Entity('exif')
|
||||
export class ExifEntity {
|
||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn()
|
||||
asset?: AssetEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
assetId!: string;
|
||||
|
||||
/* General info */
|
||||
@Column({ type: 'text', 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: 'varchar', nullable: true })
|
||||
timeZone!: string | null;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
latitude!: number | null;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
longitude!: number | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
projectionType!: string | null;
|
||||
|
||||
@Index('exif_city')
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
city!: string | null;
|
||||
|
||||
@Index('IDX_live_photo_cid')
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
livePhotoCID!: string | null;
|
||||
|
||||
@Index('IDX_auto_stack_id')
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
autoStackId!: 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 })
|
||||
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: 'varchar', nullable: true })
|
||||
exposureTime!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
profileDescription!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
colorspace!: string | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
bitsPerSample!: number | null;
|
||||
|
||||
/* Video info */
|
||||
@Column({ type: 'float8', nullable: true })
|
||||
fps?: number | null;
|
||||
|
||||
@Index('exif_text_searchable', { synchronize: false })
|
||||
@Column({
|
||||
type: 'tsvector',
|
||||
generatedType: 'STORED',
|
||||
select: false,
|
||||
asExpression: `TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))`,
|
||||
})
|
||||
exifTextSearchableColumn!: string;
|
||||
}
|
||||
44
server/src/entities/geodata-places.entity.ts
Normal file
44
server/src/entities/geodata-places.entity.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('geodata_places', { synchronize: false })
|
||||
export class GeodataPlacesEntity {
|
||||
@PrimaryColumn({ type: 'integer' })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'float' })
|
||||
longitude!: number;
|
||||
|
||||
@Column({ type: 'float' })
|
||||
latitude!: number;
|
||||
|
||||
// @Column({
|
||||
// generatedType: 'STORED',
|
||||
// asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)',
|
||||
// type: 'earth',
|
||||
// })
|
||||
// earthCoord!: unknown;
|
||||
|
||||
@Column({ type: 'char', length: 2 })
|
||||
countryCode!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
admin1Code!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 80, nullable: true })
|
||||
admin2Code!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
admin1Name!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
admin2Name!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
alternateNames!: string;
|
||||
|
||||
@Column({ type: 'date' })
|
||||
modificationDate!: Date;
|
||||
}
|
||||
47
server/src/entities/index.ts
Normal file
47
server/src/entities/index.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { ActivityEntity } from 'src/entities/activity.entity';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AuditEntity } from 'src/entities/audit.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { MoveEntity } from 'src/entities/move.entity';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export const databaseEntities = [
|
||||
ActivityEntity,
|
||||
AlbumEntity,
|
||||
APIKeyEntity,
|
||||
AssetEntity,
|
||||
AssetStackEntity,
|
||||
AssetFaceEntity,
|
||||
AssetJobStatusEntity,
|
||||
AuditEntity,
|
||||
ExifEntity,
|
||||
GeodataPlacesEntity,
|
||||
MoveEntity,
|
||||
PartnerEntity,
|
||||
PersonEntity,
|
||||
SharedLinkEntity,
|
||||
SmartInfoEntity,
|
||||
SmartSearchEntity,
|
||||
SystemConfigEntity,
|
||||
SystemMetadataEntity,
|
||||
TagEntity,
|
||||
UserEntity,
|
||||
UserTokenEntity,
|
||||
LibraryEntity,
|
||||
];
|
||||
61
server/src/entities/library.entity.ts
Normal file
61
server/src/entities/library.entity.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('libraries')
|
||||
export class LibraryEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
name!: string;
|
||||
|
||||
@OneToMany(() => AssetEntity, (asset) => asset.library)
|
||||
@JoinTable()
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
type!: LibraryType;
|
||||
|
||||
@Column('text', { array: true })
|
||||
importPaths!: string[];
|
||||
|
||||
@Column('text', { array: true })
|
||||
exclusionPatterns!: string[];
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
refreshedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: boolean;
|
||||
}
|
||||
|
||||
export enum LibraryType {
|
||||
UPLOAD = 'UPLOAD',
|
||||
EXTERNAL = 'EXTERNAL',
|
||||
}
|
||||
41
server/src/entities/move.entity.ts
Normal file
41
server/src/entities/move.entity.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
|
||||
@Entity('move_history')
|
||||
// path lock (per entity)
|
||||
@Unique('UQ_entityId_pathType', ['entityId', 'pathType'])
|
||||
// new path lock (global)
|
||||
@Unique('UQ_newPath', ['newPath'])
|
||||
export class MoveEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
entityId!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
pathType!: PathType;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
oldPath!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
newPath!: string;
|
||||
}
|
||||
|
||||
export enum AssetPathType {
|
||||
ORIGINAL = 'original',
|
||||
JPEG_THUMBNAIL = 'jpeg_thumbnail',
|
||||
WEBP_THUMBNAIL = 'webp_thumbnail',
|
||||
ENCODED_VIDEO = 'encoded_video',
|
||||
SIDECAR = 'sidecar',
|
||||
}
|
||||
|
||||
export enum PersonPathType {
|
||||
FACE = 'face',
|
||||
}
|
||||
|
||||
export enum UserPathType {
|
||||
PROFILE = 'profile',
|
||||
}
|
||||
|
||||
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
||||
28
server/src/entities/partner.entity.ts
Normal file
28
server/src/entities/partner.entity.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('partners')
|
||||
export class PartnerEntity {
|
||||
@PrimaryColumn('uuid')
|
||||
sharedById!: string;
|
||||
|
||||
@PrimaryColumn('uuid')
|
||||
sharedWithId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', eager: true })
|
||||
@JoinColumn({ name: 'sharedById' })
|
||||
sharedBy!: UserEntity;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', eager: true })
|
||||
@JoinColumn({ name: 'sharedWithId' })
|
||||
sharedWith!: UserEntity;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
inTimeline!: boolean;
|
||||
}
|
||||
52
server/src/entities/person.entity.ts
Normal file
52
server/src/entities/person.entity.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Check,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('person')
|
||||
@Check(`"birthDate" <= CURRENT_DATE`)
|
||||
export class PersonEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column({ default: '' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
birthDate!: Date | null;
|
||||
|
||||
@Column({ default: '' })
|
||||
thumbnailPath!: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
faceAssetId!: string | null;
|
||||
|
||||
@ManyToOne(() => AssetFaceEntity, { onDelete: 'SET NULL', nullable: true })
|
||||
faceAsset!: AssetFaceEntity | null;
|
||||
|
||||
@OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person)
|
||||
faces!: AssetFaceEntity[];
|
||||
|
||||
@Column({ default: false })
|
||||
isHidden!: boolean;
|
||||
}
|
||||
74
server/src/entities/shared-link.entity.ts
Normal file
74
server/src/entities/shared-link.entity.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('shared_links')
|
||||
@Unique('UQ_sharedlink_key', ['key'])
|
||||
export class SharedLinkEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
password!: string | null;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
user!: UserEntity;
|
||||
|
||||
@Index('IDX_sharedlink_key')
|
||||
@Column({ type: 'bytea' })
|
||||
key!: Buffer; // use to access the inidividual asset
|
||||
|
||||
@Column()
|
||||
type!: SharedLinkType;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
expiresAt!: Date | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
allowUpload!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
allowDownload!: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
showExif!: boolean;
|
||||
|
||||
@ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@Index('IDX_sharedlink_albumId')
|
||||
@ManyToOne(() => AlbumEntity, (album) => album.sharedLinks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
album?: AlbumEntity;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
albumId!: string | null;
|
||||
}
|
||||
|
||||
export enum SharedLinkType {
|
||||
ALBUM = 'ALBUM',
|
||||
|
||||
/**
|
||||
* Individual asset
|
||||
* or group of assets that are not in an album
|
||||
*/
|
||||
INDIVIDUAL = 'INDIVIDUAL',
|
||||
}
|
||||
18
server/src/entities/smart-info.entity.ts
Normal file
18
server/src/entities/smart-info.entity.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('smart_info', { synchronize: false })
|
||||
export class SmartInfoEntity {
|
||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
||||
asset?: AssetEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
assetId!: string;
|
||||
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
tags!: string[] | null;
|
||||
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
objects!: string[] | null;
|
||||
}
|
||||
20
server/src/entities/smart-search.entity.ts
Normal file
20
server/src/entities/smart-search.entity.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('smart_search', { synchronize: false })
|
||||
export class SmartSearchEntity {
|
||||
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
|
||||
asset?: AssetEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
assetId!: string;
|
||||
|
||||
@Index('clip_index', { synchronize: false })
|
||||
@Column({
|
||||
type: 'float4',
|
||||
array: true,
|
||||
select: false,
|
||||
})
|
||||
embedding!: number[];
|
||||
}
|
||||
284
server/src/entities/system-config.entity.ts
Normal file
284
server/src/entities/system-config.entity.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { ConcurrentQueueName } from 'src/domain/job/job.constants';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('system_config')
|
||||
export class SystemConfigEntity<T = SystemConfigValue> {
|
||||
@PrimaryColumn()
|
||||
key!: SystemConfigKey;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: T | T[];
|
||||
}
|
||||
|
||||
export type SystemConfigValue = string | number | boolean;
|
||||
|
||||
// dot notation matches path in `SystemConfig`
|
||||
export enum SystemConfigKey {
|
||||
FFMPEG_CRF = 'ffmpeg.crf',
|
||||
FFMPEG_THREADS = 'ffmpeg.threads',
|
||||
FFMPEG_PRESET = 'ffmpeg.preset',
|
||||
FFMPEG_TARGET_VIDEO_CODEC = 'ffmpeg.targetVideoCodec',
|
||||
FFMPEG_ACCEPTED_VIDEO_CODECS = 'ffmpeg.acceptedVideoCodecs',
|
||||
FFMPEG_TARGET_AUDIO_CODEC = 'ffmpeg.targetAudioCodec',
|
||||
FFMPEG_ACCEPTED_AUDIO_CODECS = 'ffmpeg.acceptedAudioCodecs',
|
||||
FFMPEG_TARGET_RESOLUTION = 'ffmpeg.targetResolution',
|
||||
FFMPEG_MAX_BITRATE = 'ffmpeg.maxBitrate',
|
||||
FFMPEG_BFRAMES = 'ffmpeg.bframes',
|
||||
FFMPEG_REFS = 'ffmpeg.refs',
|
||||
FFMPEG_GOP_SIZE = 'ffmpeg.gopSize',
|
||||
FFMPEG_NPL = 'ffmpeg.npl',
|
||||
FFMPEG_TEMPORAL_AQ = 'ffmpeg.temporalAQ',
|
||||
FFMPEG_CQ_MODE = 'ffmpeg.cqMode',
|
||||
FFMPEG_TWO_PASS = 'ffmpeg.twoPass',
|
||||
FFMPEG_PREFERRED_HW_DEVICE = 'ffmpeg.preferredHwDevice',
|
||||
FFMPEG_TRANSCODE = 'ffmpeg.transcode',
|
||||
FFMPEG_ACCEL = 'ffmpeg.accel',
|
||||
FFMPEG_TONEMAP = 'ffmpeg.tonemap',
|
||||
|
||||
JOB_THUMBNAIL_GENERATION_CONCURRENCY = 'job.thumbnailGeneration.concurrency',
|
||||
JOB_METADATA_EXTRACTION_CONCURRENCY = 'job.metadataExtraction.concurrency',
|
||||
JOB_VIDEO_CONVERSION_CONCURRENCY = 'job.videoConversion.concurrency',
|
||||
JOB_FACE_DETECTION_CONCURRENCY = 'job.faceDetection.concurrency',
|
||||
JOB_CLIP_ENCODING_CONCURRENCY = 'job.smartSearch.concurrency',
|
||||
JOB_BACKGROUND_TASK_CONCURRENCY = 'job.backgroundTask.concurrency',
|
||||
JOB_STORAGE_TEMPLATE_MIGRATION_CONCURRENCY = 'job.storageTemplateMigration.concurrency',
|
||||
JOB_SEARCH_CONCURRENCY = 'job.search.concurrency',
|
||||
JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency',
|
||||
JOB_LIBRARY_CONCURRENCY = 'job.library.concurrency',
|
||||
JOB_MIGRATION_CONCURRENCY = 'job.migration.concurrency',
|
||||
|
||||
LIBRARY_SCAN_ENABLED = 'library.scan.enabled',
|
||||
LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression',
|
||||
|
||||
LIBRARY_WATCH_ENABLED = 'library.watch.enabled',
|
||||
|
||||
LOGGING_ENABLED = 'logging.enabled',
|
||||
LOGGING_LEVEL = 'logging.level',
|
||||
|
||||
MACHINE_LEARNING_ENABLED = 'machineLearning.enabled',
|
||||
MACHINE_LEARNING_URL = 'machineLearning.url',
|
||||
|
||||
MACHINE_LEARNING_CLIP_ENABLED = 'machineLearning.clip.enabled',
|
||||
MACHINE_LEARNING_CLIP_MODEL_NAME = 'machineLearning.clip.modelName',
|
||||
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED = 'machineLearning.facialRecognition.enabled',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME = 'machineLearning.facialRecognition.modelName',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES = 'machineLearning.facialRecognition.minFaces',
|
||||
|
||||
MAP_ENABLED = 'map.enabled',
|
||||
MAP_LIGHT_STYLE = 'map.lightStyle',
|
||||
MAP_DARK_STYLE = 'map.darkStyle',
|
||||
|
||||
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
|
||||
|
||||
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
|
||||
|
||||
OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch',
|
||||
OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
|
||||
OAUTH_BUTTON_TEXT = 'oauth.buttonText',
|
||||
OAUTH_CLIENT_ID = 'oauth.clientId',
|
||||
OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
|
||||
OAUTH_DEFAULT_STORAGE_QUOTA = 'oauth.defaultStorageQuota',
|
||||
OAUTH_ENABLED = 'oauth.enabled',
|
||||
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
|
||||
OAUTH_MOBILE_OVERRIDE_ENABLED = 'oauth.mobileOverrideEnabled',
|
||||
OAUTH_MOBILE_REDIRECT_URI = 'oauth.mobileRedirectUri',
|
||||
OAUTH_SCOPE = 'oauth.scope',
|
||||
OAUTH_SIGNING_ALGORITHM = 'oauth.signingAlgorithm',
|
||||
OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim',
|
||||
OAUTH_STORAGE_QUOTA_CLAIM = 'oauth.storageQuotaClaim',
|
||||
|
||||
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
|
||||
|
||||
SERVER_EXTERNAL_DOMAIN = 'server.externalDomain',
|
||||
SERVER_LOGIN_PAGE_MESSAGE = 'server.loginPageMessage',
|
||||
|
||||
STORAGE_TEMPLATE_ENABLED = 'storageTemplate.enabled',
|
||||
STORAGE_TEMPLATE_HASH_VERIFICATION_ENABLED = 'storageTemplate.hashVerificationEnabled',
|
||||
STORAGE_TEMPLATE = 'storageTemplate.template',
|
||||
|
||||
THUMBNAIL_WEBP_SIZE = 'thumbnail.webpSize',
|
||||
THUMBNAIL_JPEG_SIZE = 'thumbnail.jpegSize',
|
||||
THUMBNAIL_QUALITY = 'thumbnail.quality',
|
||||
THUMBNAIL_COLORSPACE = 'thumbnail.colorspace',
|
||||
|
||||
TRASH_ENABLED = 'trash.enabled',
|
||||
TRASH_DAYS = 'trash.days',
|
||||
|
||||
THEME_CUSTOM_CSS = 'theme.customCss',
|
||||
|
||||
USER_DELETE_DELAY = 'user.deleteDelay',
|
||||
}
|
||||
|
||||
export enum TranscodePolicy {
|
||||
ALL = 'all',
|
||||
OPTIMAL = 'optimal',
|
||||
BITRATE = 'bitrate',
|
||||
REQUIRED = 'required',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum TranscodeTarget {
|
||||
NONE,
|
||||
AUDIO,
|
||||
VIDEO,
|
||||
ALL,
|
||||
}
|
||||
|
||||
export enum VideoCodec {
|
||||
H264 = 'h264',
|
||||
HEVC = 'hevc',
|
||||
VP9 = 'vp9',
|
||||
}
|
||||
|
||||
export enum AudioCodec {
|
||||
MP3 = 'mp3',
|
||||
AAC = 'aac',
|
||||
LIBOPUS = 'libopus',
|
||||
}
|
||||
|
||||
export enum TranscodeHWAccel {
|
||||
NVENC = 'nvenc',
|
||||
QSV = 'qsv',
|
||||
VAAPI = 'vaapi',
|
||||
RKMPP = 'rkmpp',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum ToneMapping {
|
||||
HABLE = 'hable',
|
||||
MOBIUS = 'mobius',
|
||||
REINHARD = 'reinhard',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum CQMode {
|
||||
AUTO = 'auto',
|
||||
CQP = 'cqp',
|
||||
ICQ = 'icq',
|
||||
}
|
||||
|
||||
export enum Colorspace {
|
||||
SRGB = 'srgb',
|
||||
P3 = 'p3',
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
VERBOSE = 'verbose',
|
||||
DEBUG = 'debug',
|
||||
LOG = 'log',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
threads: number;
|
||||
preset: string;
|
||||
targetVideoCodec: VideoCodec;
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
targetAudioCodec: AudioCodec;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
bframes: number;
|
||||
refs: number;
|
||||
gopSize: number;
|
||||
npl: number;
|
||||
temporalAQ: boolean;
|
||||
cqMode: CQMode;
|
||||
twoPass: boolean;
|
||||
preferredHwDevice: string;
|
||||
transcode: TranscodePolicy;
|
||||
accel: TranscodeHWAccel;
|
||||
tonemap: ToneMapping;
|
||||
};
|
||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
level: LogLevel;
|
||||
};
|
||||
machineLearning: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
clip: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
};
|
||||
facialRecognition: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
minScore: number;
|
||||
minFaces: number;
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
lightStyle: string;
|
||||
darkStyle: string;
|
||||
};
|
||||
reverseGeocoding: {
|
||||
enabled: boolean;
|
||||
};
|
||||
oauth: {
|
||||
autoLaunch: boolean;
|
||||
autoRegister: boolean;
|
||||
buttonText: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
defaultStorageQuota: number;
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
storageLabelClaim: string;
|
||||
storageQuotaClaim: string;
|
||||
};
|
||||
passwordLogin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
storageTemplate: {
|
||||
enabled: boolean;
|
||||
hashVerificationEnabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
thumbnail: {
|
||||
webpSize: number;
|
||||
jpegSize: number;
|
||||
quality: number;
|
||||
colorspace: Colorspace;
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
};
|
||||
trash: {
|
||||
enabled: boolean;
|
||||
days: number;
|
||||
};
|
||||
theme: {
|
||||
customCss: string;
|
||||
};
|
||||
library: {
|
||||
scan: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
watch: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
};
|
||||
user: {
|
||||
deleteDelay: number;
|
||||
};
|
||||
}
|
||||
20
server/src/entities/system-metadata.entity.ts
Normal file
20
server/src/entities/system-metadata.entity.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('system_metadata')
|
||||
export class SystemMetadataEntity {
|
||||
@PrimaryColumn()
|
||||
key!: string;
|
||||
|
||||
@Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: { [key: string]: unknown };
|
||||
}
|
||||
|
||||
export enum SystemMetadataKey {
|
||||
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
||||
ADMIN_ONBOARDING = 'admin-onboarding',
|
||||
}
|
||||
|
||||
export interface SystemMetadata extends Record<SystemMetadataKey, { [key: string]: unknown }> {
|
||||
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
|
||||
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
|
||||
}
|
||||
45
server/src/entities/tag.entity.ts
Normal file
45
server/src/entities/tag.entity.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
|
||||
@Entity('tags')
|
||||
@Unique('UQ_tag_name_userId', ['name', 'userId'])
|
||||
export class TagEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
type!: TagType;
|
||||
|
||||
@Column()
|
||||
name!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.tags)
|
||||
user!: UserEntity;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true })
|
||||
renameTagId!: string | null;
|
||||
|
||||
@ManyToMany(() => AssetEntity, (asset) => asset.tags)
|
||||
assets!: AssetEntity[];
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
29
server/src/entities/user-token.entity.ts
Normal file
29
server/src/entities/user-token.entity.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('user_token')
|
||||
export class UserTokenEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ select: false })
|
||||
token!: string;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||
user!: UserEntity;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ default: '' })
|
||||
deviceType!: string;
|
||||
|
||||
@Column({ default: '' })
|
||||
deviceOS!: string;
|
||||
}
|
||||
90
server/src/entities/user.entity.ts
Normal file
90
server/src/entities/user.entity.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum UserAvatarColor {
|
||||
PRIMARY = 'primary',
|
||||
PINK = 'pink',
|
||||
RED = 'red',
|
||||
YELLOW = 'yellow',
|
||||
BLUE = 'blue',
|
||||
GREEN = 'green',
|
||||
PURPLE = 'purple',
|
||||
ORANGE = 'orange',
|
||||
GRAY = 'gray',
|
||||
AMBER = 'amber',
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'active',
|
||||
REMOVING = 'removing',
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
@Entity('users')
|
||||
export class UserEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ default: '' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
avatarColor!: UserAvatarColor | null;
|
||||
|
||||
@Column({ default: false })
|
||||
isAdmin!: boolean;
|
||||
|
||||
@Column({ unique: true })
|
||||
email!: string;
|
||||
|
||||
@Column({ type: 'varchar', unique: true, default: null })
|
||||
storageLabel!: string | null;
|
||||
|
||||
@Column({ default: '', select: false })
|
||||
password?: string;
|
||||
|
||||
@Column({ default: '' })
|
||||
oauthId!: string;
|
||||
|
||||
@Column({ default: '' })
|
||||
profileImagePath!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
shouldChangePassword!: boolean;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'varchar', default: UserStatus.ACTIVE })
|
||||
status!: UserStatus;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ default: true })
|
||||
memoriesEnabled!: boolean;
|
||||
|
||||
@OneToMany(() => TagEntity, (tag) => tag.user)
|
||||
tags!: TagEntity[];
|
||||
|
||||
@OneToMany(() => AssetEntity, (asset) => asset.owner)
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@Column({ type: 'bigint', nullable: true })
|
||||
quotaSizeInBytes!: number | null;
|
||||
|
||||
@Column({ type: 'bigint', default: 0 })
|
||||
quotaUsageInBytes!: number;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue