mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-08 04:09:07 +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:
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -518,7 +518,7 @@ export class AssetService {
|
||||
where: {
|
||||
deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
|
||||
deviceId: checkDuplicateAssetDto.deviceId,
|
||||
userId: authUser.id,
|
||||
ownerId: authUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ export class CreateAssetDto {
|
||||
assetType!: AssetType;
|
||||
|
||||
@IsNotEmpty()
|
||||
createdAt!: string;
|
||||
fileCreatedAt!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
modifiedAt!: string;
|
||||
fileModifiedAt!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
isFavorite!: boolean;
|
||||
|
||||
Reference in New Issue
Block a user