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:
Zack Pollard
2023-02-19 16:44:53 +00:00
committed by GitHub
parent 000d0a08f4
commit 5ad4e5b614
65 changed files with 432 additions and 306 deletions

View File

@@ -79,7 +79,7 @@ export class AlbumRepository implements IAlbumRepository {
const queryProperties: FindManyOptions<AlbumEntity> = {
relations: { sharedUsers: true, assets: true, sharedLinks: true, owner: true },
order: { assets: { createdAt: 'ASC' }, createdAt: 'ASC' },
order: { assets: { fileCreatedAt: 'ASC' }, createdAt: 'ASC' },
};
let albumsQuery: Promise<AlbumEntity[]>;
@@ -123,7 +123,7 @@ export class AlbumRepository implements IAlbumRepository {
const albums = await this.albumRepository.find({
where: { ownerId: userId, assets: { id: assetId } },
relations: { owner: true, assets: true, sharedUsers: true },
order: { assets: { createdAt: 'ASC' } },
order: { assets: { fileCreatedAt: 'ASC' } },
});
return albums;
@@ -142,7 +142,7 @@ export class AlbumRepository implements IAlbumRepository {
},
order: {
assets: {
createdAt: 'ASC',
fileCreatedAt: 'ASC',
},
},
});

View File

@@ -19,7 +19,9 @@ import { AssetSearchDto } from './dto/asset-search.dto';
export interface IAssetRepository {
get(id: string): Promise<AssetEntity | null>;
create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity>;
create(
asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
): Promise<AssetEntity>;
remove(asset: AssetEntity): Promise<void>;
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
@@ -112,13 +114,13 @@ export class AssetRepository implements IAssetRepository {
.getMany();
}
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
async getAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
// Get asset count by AssetType
const items = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)`, 'count')
.addSelect(`asset.type`, 'type')
.where('"userId" = :userId', { userId: userId })
.where('"ownerId" = :ownerId', { ownerId: ownerId })
.andWhere('asset.isVisible = true')
.groupBy('asset.type')
.getRawMany();
@@ -149,7 +151,7 @@ export class AssetRepository implements IAssetRepository {
// Get asset entity from a list of time buckets
return await this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.where('asset.ownerId = :userId', { userId: userId })
.andWhere(`date_trunc('month', "createdAt") IN (:...buckets)`, {
buckets: [...getAssetByTimeBucketDto.timeBucket],
})
@@ -167,7 +169,7 @@ export class AssetRepository implements IAssetRepository {
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`date_trunc('month', "createdAt")`, 'timeBucket')
.where('"userId" = :userId', { userId: userId })
.where('"ownerId" = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true')
.groupBy(`date_trunc('month', "createdAt")`)
@@ -178,7 +180,7 @@ export class AssetRepository implements IAssetRepository {
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`date_trunc('day', "createdAt")`, 'timeBucket')
.where('"userId" = :userId', { userId: userId })
.where('"ownerId" = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true')
.groupBy(`date_trunc('day', "createdAt")`)
@@ -192,7 +194,7 @@ export class AssetRepository implements IAssetRepository {
async getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
return await this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.where('asset.ownerId = :userId', { userId: userId })
.andWhere('asset.isVisible = true')
.leftJoin('asset.exifInfo', 'ei')
.leftJoin('asset.smartInfo', 'si')
@@ -216,7 +218,7 @@ export class AssetRepository implements IAssetRepository {
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1
WHERE a."ownerId" = $1
AND a."isVisible" = true
AND si.objects IS NOT NULL
`,
@@ -230,7 +232,7 @@ export class AssetRepository implements IAssetRepository {
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."userId" = $1
WHERE a."ownerId" = $1
AND a."isVisible" = true
AND e.city IS NOT NULL
AND a.type = 'IMAGE';
@@ -255,12 +257,12 @@ export class AssetRepository implements IAssetRepository {
/**
* Get all assets belong to the user on the database
* @param userId
* @param ownerId
*/
async getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
async getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
return this.assetRepository.find({
where: {
userId,
ownerId,
resizePath: Not(IsNull()),
isVisible: true,
isFavorite: dto.isFavorite,
@@ -271,7 +273,7 @@ export class AssetRepository implements IAssetRepository {
},
skip: dto.skip || 0,
order: {
createdAt: 'DESC',
fileCreatedAt: 'DESC',
},
});
}
@@ -280,7 +282,9 @@ export class AssetRepository implements IAssetRepository {
return this.assetRepository.findOne({ where: { id } });
}
async create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity> {
async create(
asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
): Promise<AssetEntity> {
return this.assetRepository.save(asset);
}
@@ -304,16 +308,16 @@ export class AssetRepository implements IAssetRepository {
/**
* Get assets by device's Id on the database
* @param userId
* @param ownerId
* @param deviceId
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
async getAllByDeviceId(userId: string, deviceId: string): Promise<string[]> {
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
const rows = await this.assetRepository.find({
where: {
userId: userId,
deviceId: deviceId,
ownerId,
deviceId,
isVisible: true,
},
select: ['deviceAssetId'],
@@ -326,14 +330,14 @@ export class AssetRepository implements IAssetRepository {
/**
* Get asset by checksum on the database
* @param userId
* @param ownerId
* @param checksum
*
*/
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity> {
getAssetByChecksum(ownerId: string, checksum: Buffer): Promise<AssetEntity> {
return this.assetRepository.findOneOrFail({
where: {
userId,
ownerId,
checksum,
},
relations: ['exifInfo'],
@@ -341,7 +345,7 @@ export class AssetRepository implements IAssetRepository {
}
async getExistingAssets(
userId: string,
ownerId: string,
checkDuplicateAssetDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
const existingAssets = await this.assetRepository.find({
@@ -349,17 +353,17 @@ export class AssetRepository implements IAssetRepository {
where: {
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
deviceId: checkDuplicateAssetDto.deviceId,
userId,
ownerId,
},
});
return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId));
}
async countByIdAndUser(assetId: string, userId: string): Promise<number> {
async countByIdAndUser(assetId: string, ownerId: string): Promise<number> {
return await this.assetRepository.count({
where: {
id: assetId,
userId,
ownerId,
},
});
}

View File

@@ -1,6 +1,5 @@
import { timeUtils } from '@app/common';
import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
import { AssetEntity } from '@app/infra/db/entities';
import { AssetEntity, UserEntity } from '@app/infra/db/entities';
import { StorageService } from '@app/storage';
import { IAssetRepository } from './asset-repository';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
@@ -19,24 +18,23 @@ export class AssetCore {
livePhotoAssetId?: string,
): Promise<AssetEntity> {
let asset = await this.repository.create({
userId: authUser.id,
owner: { id: authUser.id } as UserEntity,
mimeType: file.mimeType,
checksum: file.checksum || null,
originalPath: file.originalPath,
createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(),
modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
deviceAssetId: dto.deviceAssetId,
deviceId: dto.deviceId,
fileCreatedAt: dto.fileCreatedAt,
fileModifiedAt: dto.fileModifiedAt,
type: dto.assetType,
isFavorite: dto.isFavorite,
duration: dto.duration || null,
isVisible: dto.isVisible ?? true,
livePhotoVideoId: livePhotoAssetId || null,
livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,
resizePath: null,
webpPath: null,
encodedVideoPath: null,

View File

@@ -27,8 +27,8 @@ const _getCreateAssetDto = (): CreateAssetDto => {
createAssetDto.deviceAssetId = 'deviceAssetId';
createAssetDto.deviceId = 'deviceId';
createAssetDto.assetType = AssetType.OTHER;
createAssetDto.createdAt = '2022-06-19T23:41:36.910Z';
createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.isFavorite = false;
createAssetDto.duration = '0:00:00.000000';
@@ -39,14 +39,15 @@ const _getAsset_1 = () => {
const asset_1 = new AssetEntity();
asset_1.id = 'id_1';
asset_1.userId = 'user_id_1';
asset_1.ownerId = 'user_id_1';
asset_1.deviceAssetId = 'device_asset_id_1';
asset_1.deviceId = 'device_id_1';
asset_1.type = AssetType.VIDEO;
asset_1.originalPath = 'fake_path/asset_1.jpeg';
asset_1.resizePath = '';
asset_1.createdAt = '2022-06-19T23:41:36.910Z';
asset_1.modifiedAt = '2022-06-19T23:41:36.910Z';
asset_1.fileModifiedAt = '2022-06-19T23:41:36.910Z';
asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z';
asset_1.updatedAt = '2022-06-19T23:41:36.910Z';
asset_1.isFavorite = false;
asset_1.mimeType = 'image/jpeg';
asset_1.webpPath = '';
@@ -59,14 +60,15 @@ const _getAsset_2 = () => {
const asset_2 = new AssetEntity();
asset_2.id = 'id_2';
asset_2.userId = 'user_id_1';
asset_2.ownerId = 'user_id_1';
asset_2.deviceAssetId = 'device_asset_id_2';
asset_2.deviceId = 'device_id_1';
asset_2.type = AssetType.VIDEO;
asset_2.originalPath = 'fake_path/asset_2.jpeg';
asset_2.resizePath = '';
asset_2.createdAt = '2022-06-19T23:41:36.910Z';
asset_2.modifiedAt = '2022-06-19T23:41:36.910Z';
asset_2.fileModifiedAt = '2022-06-19T23:41:36.910Z';
asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z';
asset_2.updatedAt = '2022-06-19T23:41:36.910Z';
asset_2.isFavorite = false;
asset_2.mimeType = 'image/jpeg';
asset_2.webpPath = '';
@@ -292,7 +294,7 @@ describe('AssetService', () => {
const asset = {
id: 'live-photo-asset',
originalPath: file.originalPath,
userId: authStub.user1.id,
ownerId: authStub.user1.id,
type: AssetType.IMAGE,
isVisible: true,
} as AssetEntity;
@@ -307,7 +309,7 @@ describe('AssetService', () => {
const livePhotoAsset = {
id: 'live-photo-motion',
originalPath: livePhotoFile.originalPath,
userId: authStub.user1.id,
ownerId: authStub.user1.id,
type: AssetType.VIDEO,
isVisible: false,
} as AssetEntity;

View File

@@ -518,7 +518,7 @@ export class AssetService {
where: {
deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
deviceId: checkDuplicateAssetDto.deviceId,
userId: authUser.id,
ownerId: authUser.id,
},
});

View File

@@ -16,10 +16,10 @@ export class CreateAssetDto {
assetType!: AssetType;
@IsNotEmpty()
createdAt!: string;
fileCreatedAt!: string;
@IsNotEmpty()
modifiedAt!: string;
fileModifiedAt!: string;
@IsNotEmpty()
isFavorite!: boolean;

View File

@@ -159,8 +159,8 @@ export class MetadataExtractionProcessor {
return exifDate.toDate();
};
const createdAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.createdAt);
const modifyDate = exifToDate(exifData?.ModifyDate ?? asset.modifiedAt);
const fileCreatedAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.fileCreatedAt);
const fileModifiedAt = exifToDate(exifData?.ModifyDate ?? asset.fileModifiedAt);
const fileStats = fs.statSync(asset.originalPath);
const fileSizeInBytes = fileStats.size;
@@ -174,8 +174,8 @@ export class MetadataExtractionProcessor {
newExif.exifImageWidth = exifData?.ExifImageWidth || exifData?.ImageWidth || null;
newExif.exposureTime = exifData?.ExposureTime || null;
newExif.orientation = exifData?.Orientation?.toString() || null;
newExif.dateTimeOriginal = createdAt;
newExif.modifyDate = modifyDate;
newExif.dateTimeOriginal = fileCreatedAt;
newExif.modifyDate = fileModifiedAt;
newExif.lensModel = exifData?.LensModel || null;
newExif.fNumber = exifData?.FNumber || null;
newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null;
@@ -186,7 +186,7 @@ export class MetadataExtractionProcessor {
await this.assetRepository.save({
id: asset.id,
createdAt: createdAt?.toISOString(),
fileCreatedAt: fileCreatedAt?.toISOString(),
});
if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
@@ -273,7 +273,7 @@ export class MetadataExtractionProcessor {
}),
);
let durationString = asset.duration;
let createdAt = asset.createdAt;
let fileCreatedAt = asset.fileCreatedAt;
if (data.format.duration) {
durationString = this.extractDuration(data.format.duration);
@@ -282,14 +282,10 @@ export class MetadataExtractionProcessor {
const videoTags = data.format.tags;
if (videoTags) {
if (videoTags['com.apple.quicktime.creationdate']) {
createdAt = String(videoTags['com.apple.quicktime.creationdate']);
fileCreatedAt = String(videoTags['com.apple.quicktime.creationdate']);
} else if (videoTags['creation_time']) {
createdAt = String(videoTags['creation_time']);
} else {
createdAt = asset.createdAt;
fileCreatedAt = String(videoTags['creation_time']);
}
} else {
createdAt = asset.createdAt;
}
const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
@@ -302,7 +298,7 @@ export class MetadataExtractionProcessor {
newExif.description = '';
newExif.imageName = path.parse(fileName).name || null;
newExif.fileSizeInByte = data.format.size || null;
newExif.dateTimeOriginal = createdAt ? new Date(createdAt) : null;
newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
newExif.modifyDate = null;
newExif.latitude = null;
newExif.longitude = null;
@@ -382,8 +378,9 @@ export class MetadataExtractionProcessor {
}
await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
await this.assetRepository.update({ id: asset.id }, { duration: durationString, createdAt: createdAt });
await this.assetRepository.update({ id: asset.id }, { duration: durationString, fileCreatedAt });
} catch (err) {
``;
// do nothing
console.log('Error in video metadata extraction', err);
}

View File

@@ -40,7 +40,7 @@ export class ThumbnailGeneratorProcessor {
const { asset } = job.data;
const sanitizedDeviceId = sanitize(String(asset.deviceId));
const resizePath = join(basePath, asset.userId, 'thumb', sanitizedDeviceId);
const resizePath = join(basePath, asset.ownerId, 'thumb', sanitizedDeviceId);
if (!existsSync(resizePath)) {
mkdirSync(resizePath, { recursive: true });
@@ -75,7 +75,7 @@ export class ThumbnailGeneratorProcessor {
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
}
if (asset.type == AssetType.VIDEO) {
@@ -106,7 +106,7 @@ export class ThumbnailGeneratorProcessor {
await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
}
}

View File

@@ -61,7 +61,7 @@ export class UserDeletionProcessor {
await this.albumRepository.remove(albums);
await this.apiKeyRepository.delete({ userId: user.id });
await this.assetRepository.delete({ userId: user.id });
await this.assetRepository.delete({ ownerId: user.id });
await this.userRepository.remove(user);
} catch (error: any) {
this.logger.error(`Failed to remove user`);

View File

@@ -22,7 +22,7 @@ export class VideoTranscodeProcessor {
async videoConversion(job: Job<IVideoConversionProcessor>) {
const { asset } = job.data;
const basePath = APP_UPLOAD_LOCATION;
const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`;
const encodedVideoPath = `${basePath}/${asset.ownerId}/encoded-video`;
if (!existsSync(encodedVideoPath)) {
mkdirSync(encodedVideoPath, { recursive: true });

View File

@@ -3324,10 +3324,10 @@
"type": "string",
"nullable": true
},
"createdAt": {
"fileCreatedAt": {
"type": "string"
},
"modifiedAt": {
"fileModifiedAt": {
"type": "string"
},
"updatedAt": {
@@ -3376,8 +3376,8 @@
"deviceId",
"originalPath",
"resizePath",
"createdAt",
"modifiedAt",
"fileCreatedAt",
"fileModifiedAt",
"updatedAt",
"isFavorite",
"mimeType",
@@ -3817,10 +3817,10 @@
"deviceId": {
"type": "string"
},
"createdAt": {
"fileCreatedAt": {
"type": "string"
},
"modifiedAt": {
"fileModifiedAt": {
"type": "string"
},
"isFavorite": {
@@ -3841,8 +3841,8 @@
"assetData",
"deviceAssetId",
"deviceId",
"createdAt",
"modifiedAt",
"fileCreatedAt",
"fileModifiedAt",
"isFavorite",
"fileExtension"
]

View File

@@ -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,

View File

@@ -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,

View File

@@ -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[];
}

View File

@@ -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`);
}
}

View File

@@ -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"`);
}
}

View File

@@ -31,11 +31,11 @@ export class SharedLinkRepository implements ISharedLinkRepository {
order: {
createdAt: 'DESC',
assets: {
createdAt: 'ASC',
fileCreatedAt: 'ASC',
},
album: {
assets: {
createdAt: 'ASC',
fileCreatedAt: 'ASC',
},
},
},

View File

@@ -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,

View File

@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "immich",
"version": "1.46.1",
"version": "1.47.3",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/bull": "^0.6.2",