mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-01-19 23:56:57 +00:00
infra(server)!: fix typeorm asset entity relations (#1782)
* fix: add correct relations to asset typeorm entity * fix: add missing createdAt column to asset entity * ci: run check to make sure generated API is up-to-date * ci: cancel workflows that aren't for the latest commit in a branch * chore: add fvm config for flutter
This commit is contained in:
@@ -14,8 +14,8 @@ export class AssetResponseDto {
|
||||
type!: AssetType;
|
||||
originalPath!: string;
|
||||
resizePath!: string | null;
|
||||
createdAt!: string;
|
||||
modifiedAt!: string;
|
||||
fileCreatedAt!: string;
|
||||
fileModifiedAt!: string;
|
||||
updatedAt!: string;
|
||||
isFavorite!: boolean;
|
||||
mimeType!: string | null;
|
||||
@@ -32,13 +32,13 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
deviceAssetId: entity.deviceAssetId,
|
||||
ownerId: entity.userId,
|
||||
ownerId: entity.ownerId,
|
||||
deviceId: entity.deviceId,
|
||||
type: entity.type,
|
||||
originalPath: entity.originalPath,
|
||||
resizePath: entity.resizePath,
|
||||
createdAt: entity.createdAt,
|
||||
modifiedAt: entity.modifiedAt,
|
||||
fileCreatedAt: entity.fileCreatedAt,
|
||||
fileModifiedAt: entity.fileModifiedAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
isFavorite: entity.isFavorite,
|
||||
mimeType: entity.mimeType,
|
||||
@@ -56,13 +56,13 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
deviceAssetId: entity.deviceAssetId,
|
||||
ownerId: entity.userId,
|
||||
ownerId: entity.ownerId,
|
||||
deviceId: entity.deviceId,
|
||||
type: entity.type,
|
||||
originalPath: entity.originalPath,
|
||||
resizePath: entity.resizePath,
|
||||
createdAt: entity.createdAt,
|
||||
modifiedAt: entity.modifiedAt,
|
||||
fileCreatedAt: entity.fileCreatedAt,
|
||||
fileModifiedAt: entity.fileModifiedAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
isFavorite: entity.isFavorite,
|
||||
mimeType: entity.mimeType,
|
||||
|
||||
@@ -95,20 +95,23 @@ export const assetEntityStub = {
|
||||
image: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
modifiedAt: today.toISOString(),
|
||||
createdAt: today.toISOString(),
|
||||
userId: 'user-id',
|
||||
fileModifiedAt: today.toISOString(),
|
||||
fileCreatedAt: today.toISOString(),
|
||||
owner: userEntityStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path',
|
||||
resizePath: null,
|
||||
type: AssetType.IMAGE,
|
||||
webpPath: null,
|
||||
encodedVideoPath: null,
|
||||
createdAt: today.toISOString(),
|
||||
updatedAt: today.toISOString(),
|
||||
mimeType: null,
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
isVisible: true,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
@@ -146,8 +149,8 @@ const assetResponse: AssetResponseDto = {
|
||||
type: AssetType.VIDEO,
|
||||
originalPath: 'fake_path/jpeg',
|
||||
resizePath: '',
|
||||
createdAt: today.toISOString(),
|
||||
modifiedAt: today.toISOString(),
|
||||
fileModifiedAt: today.toISOString(),
|
||||
fileCreatedAt: today.toISOString(),
|
||||
updatedAt: today.toISOString(),
|
||||
isFavorite: false,
|
||||
mimeType: 'image/jpeg',
|
||||
@@ -374,14 +377,16 @@ export const sharedLinkStub = {
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
userId: 'user_id_1',
|
||||
owner: userEntityStub.user1,
|
||||
ownerId: 'user_id_1',
|
||||
deviceAssetId: 'device_asset_id_1',
|
||||
deviceId: 'device_id_1',
|
||||
type: AssetType.VIDEO,
|
||||
originalPath: 'fake_path/jpeg',
|
||||
resizePath: '',
|
||||
fileModifiedAt: today.toISOString(),
|
||||
fileCreatedAt: today.toISOString(),
|
||||
createdAt: today.toISOString(),
|
||||
modifiedAt: today.toISOString(),
|
||||
updatedAt: today.toISOString(),
|
||||
isFavorite: false,
|
||||
mimeType: 'image/jpeg',
|
||||
@@ -396,6 +401,7 @@ export const sharedLinkStub = {
|
||||
encodedVideoPath: '',
|
||||
duration: null,
|
||||
isVisible: true,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
exifInfo: {
|
||||
livePhotoCID: null,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
@@ -13,9 +16,10 @@ import { ExifEntity } from './exif.entity';
|
||||
import { SharedLinkEntity } from './shared-link.entity';
|
||||
import { SmartInfoEntity } from './smart-info.entity';
|
||||
import { TagEntity } from './tag.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
@Entity('assets')
|
||||
@Unique('UQ_userid_checksum', ['userId', 'checksum'])
|
||||
@Unique('UQ_userid_checksum', ['owner', 'checksum'])
|
||||
export class AssetEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
@@ -23,8 +27,11 @@ export class AssetEntity {
|
||||
@Column()
|
||||
deviceAssetId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
deviceId!: string;
|
||||
@@ -44,15 +51,18 @@ export class AssetEntity {
|
||||
@Column({ type: 'varchar', nullable: true, default: '' })
|
||||
encodedVideoPath!: string | null;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: string;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
modifiedAt!: string;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: string;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileCreatedAt!: string;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileModifiedAt!: string;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isFavorite!: boolean;
|
||||
|
||||
@@ -69,7 +79,11 @@ export class AssetEntity {
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: boolean;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
@OneToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||
@JoinColumn()
|
||||
livePhotoVideo!: AssetEntity | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
livePhotoVideoId!: string | null;
|
||||
|
||||
@OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
|
||||
@@ -78,12 +92,11 @@ export class AssetEntity {
|
||||
@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 })
|
||||
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true, eager: true })
|
||||
@JoinTable({ name: 'tag_asset' })
|
||||
tags!: TagEntity[];
|
||||
|
||||
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
|
||||
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true, eager: true })
|
||||
@JoinTable({ name: 'shared_link__asset' })
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class FixAssetRelations1676680127415 implements MigrationInterface {
|
||||
name = 'FixAssetRelations1676680127415'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "modifiedAt" TO "fileModifiedAt"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "createdAt" TO "fileCreatedAt"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "userId" TO "ownerId"`);
|
||||
await queryRunner.query(`ALTER TABLE assets ALTER COLUMN "ownerId" TYPE uuid USING "ownerId"::uuid;`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef" UNIQUE ("livePhotoVideoId")`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_16294b83fa8c0149719a1f631ef"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "fileCreatedAt" TO "createdAt"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "fileModifiedAt" TO "modifiedAt"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "ownerId" TO "userId"`);
|
||||
await queryRunner.query(`ALTER TABLE assets ALTER COLUMN "userId" TYPE varchar`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AssetCreatedAtField1676721296440 implements MigrationInterface {
|
||||
name = 'AssetCreatedAtField1676721296440'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "createdAt"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,11 +31,11 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
assets: {
|
||||
createdAt: 'ASC',
|
||||
fileCreatedAt: 'ASC',
|
||||
},
|
||||
album: {
|
||||
assets: {
|
||||
createdAt: 'ASC',
|
||||
fileCreatedAt: 'ASC',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ export class StorageService {
|
||||
const source = asset.originalPath;
|
||||
const ext = path.extname(source).split('.').pop() as string;
|
||||
const sanitized = sanitize(path.basename(filename, `.${ext}`));
|
||||
const rootPath = path.join(APP_UPLOAD_LOCATION, asset.userId);
|
||||
const rootPath = path.join(APP_UPLOAD_LOCATION, asset.ownerId);
|
||||
const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
|
||||
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
||||
let destination = `${fullPath}.${ext}`;
|
||||
@@ -132,7 +132,7 @@ export class StorageService {
|
||||
this.render(
|
||||
template,
|
||||
{
|
||||
createdAt: new Date().toISOString(),
|
||||
fileCreatedAt: new Date().toISOString(),
|
||||
originalPath: '/upload/test/IMG_123.jpg',
|
||||
type: AssetType.IMAGE,
|
||||
} as AssetEntity,
|
||||
@@ -161,7 +161,7 @@ export class StorageService {
|
||||
const fileType = asset.type == AssetType.IMAGE ? 'IMG' : 'VID';
|
||||
const fileTypeFull = asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO';
|
||||
|
||||
const dt = luxon.DateTime.fromISO(new Date(asset.createdAt).toISOString());
|
||||
const dt = luxon.DateTime.fromISO(new Date(asset.fileCreatedAt).toISOString());
|
||||
|
||||
const dateTokens = [
|
||||
...supportedYearTokens,
|
||||
|
||||
Reference in New Issue
Block a user